Room list on a phone: plain text rows in the heading type; no Create Room or Join with Address #40
5 changed files with 174 additions and 36 deletions
|
|
@ -158,6 +158,7 @@ export const BubbleContentArrowLeft = style({
|
|||
const PHONE = 'screen and (max-width: 700px)';
|
||||
const PHONE_PORTRAIT = 'min(38vw, 160px)';
|
||||
const PHONE_TEXT_FLOOR = toRem(170);
|
||||
const PORTRAIT_OVERHANG = 58;
|
||||
const PORTRAIT = toRem(124);
|
||||
const POSE_GUTTER = toRem(14);
|
||||
const BUBBLE_CORNER = toRem(6);
|
||||
|
|
@ -180,7 +181,6 @@ export const BubbleRowPortrait = style({
|
|||
'@media': {
|
||||
[PHONE]: {
|
||||
minHeight: 0,
|
||||
marginTop: toRem(58),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -207,6 +207,12 @@ export const BubbleHeaderPose = style({
|
|||
'@media': {
|
||||
[PHONE]: {
|
||||
alignSelf: 'flex-end',
|
||||
// The band above the bubble where the portrait overhangs: the header sits at its foot, beside
|
||||
// the portrait, so the overhang needs no margin of its own.
|
||||
minHeight: toRem(PORTRAIT_OVERHANG),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
margin: `0 ${toRem(14)} ${toRem(2)}`,
|
||||
width: 'max-content',
|
||||
maxWidth: `calc(100vw - ${PHONE_PORTRAIT} - ${toRem(64)})`,
|
||||
|
|
@ -224,6 +230,15 @@ export const BubbleHeaderPoseAlt = style({
|
|||
},
|
||||
});
|
||||
|
||||
// A portrait row with no header (nothing to fill the overhang band) keeps the band as a margin.
|
||||
globalStyle(`${BubbleRowPortrait}:not(:has(.${BubbleHeaderPose}))`, {
|
||||
'@media': {
|
||||
[PHONE]: {
|
||||
marginTop: toRem(PORTRAIT_OVERHANG),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const BubbleColumnPose = style({
|
||||
position: 'relative',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ComplexStyleRule, createVar, style } from '@vanilla-extract/css';
|
||||
import { ComplexStyleRule, StyleRule, createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
|
||||
import { ContainerColor, DefaultReset, Disabled, RadiiVariant, color, config, toRem } from 'folds';
|
||||
import { surfaceInk } from '../../features/scene/sceneShell.css';
|
||||
|
|
@ -20,11 +20,16 @@ export const NavCategory = style([
|
|||
|
||||
export const NavCategoryHeader = style({
|
||||
gap: config.space.S100,
|
||||
selectors: {
|
||||
// Category labels sit on the scene like the rows do, and need a wash to be read.
|
||||
':root[data-scene] &': {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_LABEL}, transparent)`,
|
||||
borderRadius: config.radii.R400,
|
||||
'@media': {
|
||||
// Category labels sit on the scene like the rows do, and need a wash to be read (wide screens;
|
||||
// a phone's list is plain text).
|
||||
'screen and (min-width: 751px)': {
|
||||
selectors: {
|
||||
':root[data-scene] &': {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_LABEL}, transparent)`,
|
||||
borderRadius: config.radii.R400,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -60,6 +65,12 @@ const getVariant = (variant: ContainerColor): ComplexStyleRule => ({
|
|||
},
|
||||
});
|
||||
|
||||
// The room list is plain text on a phone: no pill, slab or border behind any row, and hover,
|
||||
// selected and pressed show in the label (NavLabel), never in a box. Wider screens keep the rows
|
||||
// and the wash.
|
||||
const WIDE = 'screen and (min-width: 751px)';
|
||||
const PHONE = 'screen and (max-width: 750px)';
|
||||
|
||||
const NavItemBase = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
|
|
@ -70,35 +81,45 @@ const NavItemBase = style({
|
|||
outline: 'none',
|
||||
minHeight: toRem(36),
|
||||
|
||||
selectors: {
|
||||
'&:hover, &:focus-visible': {
|
||||
backgroundColor: ContainerHover,
|
||||
'@media': {
|
||||
[PHONE]: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
},
|
||||
'&[data-hover=true]': {
|
||||
backgroundColor: ContainerHover,
|
||||
},
|
||||
// While the background scene runs the shell is clear (sceneShell.css.ts); a row is a faint
|
||||
// wash of the theme's own surface: the terrain stays visible, the text stays readable. Hover,
|
||||
// the selected row and a press each strengthen it a step.
|
||||
':root[data-scene] &': {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_ROW}, transparent)`,
|
||||
},
|
||||
':root[data-scene] &:hover, :root[data-scene] &:focus-visible, :root[data-scene] &[data-hover=true]':
|
||||
{
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_HOVER}, transparent)`,
|
||||
[WIDE]: {
|
||||
selectors: {
|
||||
'&:hover, &:focus-visible': {
|
||||
backgroundColor: ContainerHover,
|
||||
},
|
||||
'&[data-hover=true]': {
|
||||
backgroundColor: ContainerHover,
|
||||
},
|
||||
// While the background scene runs the shell is clear (sceneShell.css.ts); a row is a faint
|
||||
// wash of the theme's own surface: the terrain stays visible, the text stays readable.
|
||||
// Hover, the selected row and a press each strengthen it a step.
|
||||
':root[data-scene] &': {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_ROW}, transparent)`,
|
||||
},
|
||||
':root[data-scene] &:hover, :root[data-scene] &:focus-visible, :root[data-scene] &[data-hover=true]':
|
||||
{
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_HOVER}, transparent)`,
|
||||
},
|
||||
':root[data-scene] &[aria-selected=true]': {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_SELECTED}, transparent)`,
|
||||
},
|
||||
[`:root[data-scene] &:has(.${NavLink}:active)`]: {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_PRESSED}, transparent)`,
|
||||
},
|
||||
[`&:has(.${NavLink}:active)`]: {
|
||||
backgroundColor: ContainerActive,
|
||||
},
|
||||
'&[aria-selected=true]': {
|
||||
backgroundColor: ContainerActive,
|
||||
},
|
||||
},
|
||||
':root[data-scene] &[aria-selected=true]': {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_SELECTED}, transparent)`,
|
||||
},
|
||||
[`:root[data-scene] &:has(.${NavLink}:active)`]: {
|
||||
backgroundColor: `color-mix(in srgb, ${surfaceInk} ${SCRIM_PRESSED}, transparent)`,
|
||||
},
|
||||
[`&:has(.${NavLink}:active)`]: {
|
||||
backgroundColor: ContainerActive,
|
||||
},
|
||||
'&[aria-selected=true]': {
|
||||
backgroundColor: ContainerActive,
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
[`&:has(.${NavLink}:focus-visible)`]: {
|
||||
outline: `${config.borderWidth.B600} solid ${ContainerLine}`,
|
||||
outlineOffset: `calc(-1 * ${config.borderWidth.B600})`,
|
||||
|
|
@ -158,3 +179,76 @@ export const NavItemContent = style({
|
|||
export const NavItemOptions = style({
|
||||
paddingRight: config.space.S200,
|
||||
});
|
||||
|
||||
// A room's name on a phone wears the section headings' type (small caps-height, uppercase, 600,
|
||||
// at heading strength) and states itself in the text: hover and focus underline it, the selected
|
||||
// row is bolder and underlined, a press dims it, an unread room is at full strength. Over the scene
|
||||
// a halo of the theme's own surface keeps it readable without a box.
|
||||
export const NavLabel = style({});
|
||||
const label = `.${NavLabel}.${NavLabel}`;
|
||||
const phoneLabel = (selector: string, rule: StyleRule) =>
|
||||
globalStyle(selector, { '@media': { [PHONE]: rule } });
|
||||
|
||||
phoneLabel(label, {
|
||||
fontSize: toRem(12),
|
||||
lineHeight: toRem(16),
|
||||
fontWeight: config.fontWeight.W600,
|
||||
textTransform: 'uppercase',
|
||||
opacity: 0.75,
|
||||
textDecorationThickness: '1px',
|
||||
textUnderlineOffset: toRem(4),
|
||||
});
|
||||
phoneLabel(
|
||||
[
|
||||
`.${NavItemBase}:hover ${label}`,
|
||||
`.${NavItemBase}:focus-visible ${label}`,
|
||||
`.${NavItemBase}[data-hover=true] ${label}`,
|
||||
`.${NavItemBase}[data-highlight=true] ${label}`,
|
||||
].join(', '),
|
||||
{ opacity: 1 }
|
||||
);
|
||||
phoneLabel(`.${NavItemBase}:hover ${label}, .${NavItemBase}:focus-visible ${label}`, {
|
||||
textDecorationLine: 'underline',
|
||||
});
|
||||
phoneLabel(`.${NavItemBase}[aria-selected=true] ${label}`, {
|
||||
opacity: 1,
|
||||
fontWeight: config.fontWeight.W700,
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationThickness: '2px',
|
||||
});
|
||||
phoneLabel(`.${NavItemBase}:has(.${NavLink}:active) ${label}`, {
|
||||
opacity: 0.6,
|
||||
textDecorationLine: 'underline',
|
||||
});
|
||||
phoneLabel(`:root[data-scene] ${label}`, {
|
||||
textShadow: `0 0 3px ${surfaceInk}, 0 0 8px ${surfaceInk}`,
|
||||
});
|
||||
|
||||
// Icons, dots and the Join action share the label's halo over the scene.
|
||||
globalStyle(`:root[data-scene] ${NavItemBase} svg, :root[data-scene] ${NavItemBase} button`, {
|
||||
'@media': {
|
||||
[PHONE]: {
|
||||
filter: `drop-shadow(0 0 3px ${surfaceInk})`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// The Join action on a phone is a plain underlined word, not a pill.
|
||||
export const NavAction = style({
|
||||
'@media': {
|
||||
[PHONE]: {
|
||||
selectors: {
|
||||
'&&': {
|
||||
backgroundColor: 'transparent',
|
||||
boxShadow: 'none',
|
||||
border: 'none',
|
||||
borderRadius: 0,
|
||||
minWidth: 0,
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: toRem(4),
|
||||
flexShrink: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
NavItem,
|
||||
NavItemContent,
|
||||
} from '../../components/nav';
|
||||
import { NavAction, NavLabel } from '../../components/nav/styles.css';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { JoinAddressPrompt } from '../../components/join-address-prompt';
|
||||
import { RoomNavCategoryButton, RoomNavItem } from '../room-nav';
|
||||
|
|
@ -80,7 +81,7 @@ function JoinRow({ room }: { room: EstateRoom }) {
|
|||
<Avatar size="200" radii="400">
|
||||
<Icon src={Icons.Hash} size="100" style={{ opacity: config.opacity.P300 }} />
|
||||
</Avatar>
|
||||
<Text as="span" size="Inherit" truncate>
|
||||
<Text className={NavLabel} as="span" size="Inherit" truncate>
|
||||
{room.name}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
@ -95,6 +96,7 @@ function JoinRow({ room }: { room: EstateRoom }) {
|
|||
</Text>
|
||||
)}
|
||||
<Chip
|
||||
className={NavAction}
|
||||
variant={room.state === 'invited' ? 'Primary' : 'Secondary'}
|
||||
fill="Soft"
|
||||
radii="Pill"
|
||||
|
|
@ -281,6 +283,8 @@ export function EstateNavigator() {
|
|||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const mx = useMatrixClient();
|
||||
const title = (rootId && mx.getRoom(rootId)?.name) || 'GameCult';
|
||||
// A phone's list is the estate and nothing else: no Create Room, no Join with Address.
|
||||
const mobile = useScreenSizeContext() === ScreenSize.Mobile;
|
||||
|
||||
return (
|
||||
<PageNav>
|
||||
|
|
@ -302,7 +306,7 @@ export function EstateNavigator() {
|
|||
</PageNavHeader>
|
||||
<PageNavContent scrollRef={scrollRef}>
|
||||
<Box direction="Column" gap="300" className={css.Targets}>
|
||||
<EntryPoints />
|
||||
{!mobile && <EntryPoints />}
|
||||
{rootId && <EstateTiers rootId={rootId} />}
|
||||
{!rootId && error && (
|
||||
<EstateError message="Could not find the GameCult space. Check your connection and reload." />
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useFocusWithin, useHover } from 'react-aria';
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
||||
import { NavLabel } from '../../components/nav/styles.css';
|
||||
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
import { getDirectRoomAvatarUrl, getRoomAvatarUrl, getStateEvent } from '../../utils/room';
|
||||
|
|
@ -358,7 +359,13 @@ export function RoomNavItem({
|
|||
)}
|
||||
</Avatar>
|
||||
<Box as="span" grow="Yes">
|
||||
<Text priority={unread ? '500' : '300'} as="span" size="Inherit" truncate>
|
||||
<Text
|
||||
className={NavLabel}
|
||||
priority={unread ? '500' : '300'}
|
||||
as="span"
|
||||
size="Inherit"
|
||||
truncate
|
||||
>
|
||||
{roomName}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,26 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
import { surfaceInk } from '../scene/sceneShell.css';
|
||||
|
||||
export const CategoryButton = style({
|
||||
flexGrow: 1,
|
||||
'@media': {
|
||||
// A section heading on a phone is plain text: no pill behind it.
|
||||
'screen and (max-width: 750px)': {
|
||||
selectors: {
|
||||
'&&': {
|
||||
backgroundColor: 'transparent',
|
||||
boxShadow: 'none',
|
||||
border: 'none',
|
||||
borderRadius: 0,
|
||||
},
|
||||
':root[data-scene] &&': {
|
||||
textShadow: `0 0 3px ${surfaceInk}, 0 0 8px ${surfaceInk}`,
|
||||
filter: `drop-shadow(0 0 3px ${surfaceInk})`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export const CategoryButtonIcon = style({
|
||||
opacity: config.opacity.P400,
|
||||
|
|
|
|||
Loading…
Reference in a new issue