diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts
index 815c579f..a724241c 100644
--- a/src/app/components/message/layout/layout.css.ts
+++ b/src/app/components/message/layout/layout.css.ts
@@ -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',
});
diff --git a/src/app/components/nav/styles.css.ts b/src/app/components/nav/styles.css.ts
index b123f9bc..feee3276 100644
--- a/src/app/components/nav/styles.css.ts
+++ b/src/app/components/nav/styles.css.ts
@@ -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,
+ },
+ },
+ },
+ },
+});
diff --git a/src/app/features/estate/EstateNavigator.tsx b/src/app/features/estate/EstateNavigator.tsx
index 29451c92..1a9ca8ee 100644
--- a/src/app/features/estate/EstateNavigator.tsx
+++ b/src/app/features/estate/EstateNavigator.tsx
@@ -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 }) {
-
+
{room.name}
@@ -95,6 +96,7 @@ function JoinRow({ room }: { room: EstateRoom }) {
)}
(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 (
@@ -302,7 +306,7 @@ export function EstateNavigator() {
-
+ {!mobile && }
{rootId && }
{!rootId && error && (
diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx
index 08341113..ff23930e 100644
--- a/src/app/features/room-nav/RoomNavItem.tsx
+++ b/src/app/features/room-nav/RoomNavItem.tsx
@@ -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({
)}
-
+
{roomName}
diff --git a/src/app/features/room-nav/styles.css.ts b/src/app/features/room-nav/styles.css.ts
index b5701a6f..b9655923 100644
--- a/src/app/features/room-nav/styles.css.ts
+++ b/src/app/features/room-nav/styles.css.ts
@@ -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,