Navigator: one gutter column, constant nesting step, quieter room icons #92
7 changed files with 123 additions and 51 deletions
12
src/app/components/nav/NavGutter.tsx
Normal file
12
src/app/components/nav/NavGutter.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import * as css from './styles.css';
|
||||
|
||||
/** The fixed-width first column of a navigator row or section header; its content is centred in it. */
|
||||
export function NavGutter({ children }: { children: ReactNode }) {
|
||||
return <span className={css.NavGutter}>{children}</span>;
|
||||
}
|
||||
|
||||
/** One nesting step: everything inside is indented by the same constant. */
|
||||
export function NavLevel({ children }: { children: ReactNode }) {
|
||||
return <div className={css.NavLevel}>{children}</div>;
|
||||
}
|
||||
|
|
@ -4,3 +4,4 @@ export * from './NavEmptyLayout';
|
|||
export * from './NavItem';
|
||||
export * from './NavItemContent';
|
||||
export * from './NavItemOptions';
|
||||
export * from './NavGutter';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,29 @@ const SCRIM_HOVER = '14%';
|
|||
const SCRIM_SELECTED = '22%';
|
||||
const SCRIM_PRESSED = '48%';
|
||||
|
||||
// The navigator's one column grid. Every row, section header and join row begins with a gutter of
|
||||
// the same width, with its chevron, room icon or avatar centred in it, so every label starts at
|
||||
// the same x for its depth; each nesting level (NavLevel) indents by one constant step.
|
||||
const GUTTER = toRem(32);
|
||||
const LEVEL_STEP = toRem(16);
|
||||
|
||||
export const NavGutter = style({
|
||||
width: GUTTER,
|
||||
minWidth: GUTTER,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
export const NavLevel = style({
|
||||
paddingLeft: LEVEL_STEP,
|
||||
});
|
||||
|
||||
// Chevrons and room icons share one resting weight, so a foldout is not outweighed by its rooms.
|
||||
export const NAV_ICON_REST_OPACITY = 0.6;
|
||||
export const NAV_ICON_UNREAD_OPACITY = 0.9;
|
||||
|
||||
export const NavCategory = style([
|
||||
DefaultReset,
|
||||
{
|
||||
|
|
@ -22,6 +45,11 @@ export const NavCategory = style([
|
|||
|
||||
export const NavCategoryHeader = style({
|
||||
gap: config.space.S100,
|
||||
selectors: {
|
||||
// The header's own side padding would shift its gutter off the rows': the section button
|
||||
// carries the same left inset as NavItemContent instead.
|
||||
'&&': { paddingLeft: 0 },
|
||||
},
|
||||
'@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).
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import React, { MouseEventHandler, ReactNode, useCallback, useMemo, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { Avatar, Box, Chip, Icon, IconButton, Icons, Spinner, Text, color, config } from 'folds';
|
||||
import { Avatar, Box, Chip, Icon, IconButton, Icons, Spinner, Text, color } from 'folds';
|
||||
import { PageNav, PageNavContent, PageNavHeader } from '../../components/page';
|
||||
import {
|
||||
NavButton,
|
||||
NavCategory,
|
||||
NavCategoryHeader,
|
||||
NavGutter,
|
||||
NavItem,
|
||||
NavItemContent,
|
||||
NavLevel,
|
||||
} from '../../components/nav';
|
||||
import { NavAction, NavLabel } from '../../components/nav/styles.css';
|
||||
import { NAV_ICON_REST_OPACITY, 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';
|
||||
|
|
@ -77,12 +79,14 @@ function JoinRow({ room }: { room: EstateRoom }) {
|
|||
|
||||
return (
|
||||
<NavItem variant="Background" radii="400">
|
||||
<Box grow="Yes" alignItems="Center" gap="200" style={{ padding: '0 8px', minHeight: 38 }}>
|
||||
<Box grow="Yes" alignItems="Center" gap="200" style={{ padding: '0 8px 0 0', minHeight: 38 }}>
|
||||
<NavItemContent>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon src={Icons.Hash} size="100" style={{ opacity: config.opacity.P300 }} />
|
||||
</Avatar>
|
||||
<NavGutter>
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon src={Icons.Hash} size="100" style={{ opacity: NAV_ICON_REST_OPACITY }} />
|
||||
</Avatar>
|
||||
</NavGutter>
|
||||
<Text className={NavLabel} as="span" size="Inherit" truncate>
|
||||
{room.name}
|
||||
</Text>
|
||||
|
|
@ -166,7 +170,7 @@ function Collapsible({ categoryId, label, rooms, trailing, children }: Collapsib
|
|||
</RoomNavCategoryButton>
|
||||
{trailing}
|
||||
</NavCategoryHeader>
|
||||
{!closed && children}
|
||||
{!closed && <NavLevel>{children}</NavLevel>}
|
||||
</NavCategory>
|
||||
);
|
||||
}
|
||||
|
|
@ -206,7 +210,13 @@ function Browse({ tierId, groups }: { tierId: string; groups: EstateGroup[] }) {
|
|||
{`Browse ${count} more ${count === 1 ? 'room' : 'rooms'}`}
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
{open && groups.map((g) => <GroupSection key={g.mind} tierId={tierId} group={g} />)}
|
||||
{open && (
|
||||
<NavLevel>
|
||||
{groups.map((g) => (
|
||||
<GroupSection key={g.mind} tierId={tierId} group={g} />
|
||||
))}
|
||||
</NavLevel>
|
||||
)}
|
||||
</NavCategory>
|
||||
);
|
||||
}
|
||||
|
|
@ -235,9 +245,11 @@ function EntryPoints() {
|
|||
<NavButton onClick={() => navigate(getHomeCreatePath())}>
|
||||
<NavItemContent>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon src={Icons.Plus} size="100" />
|
||||
</Avatar>
|
||||
<NavGutter>
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon src={Icons.Plus} size="100" style={{ opacity: NAV_ICON_REST_OPACITY }} />
|
||||
</Avatar>
|
||||
</NavGutter>
|
||||
<Text as="span" size="Inherit" truncate>
|
||||
Create Room
|
||||
</Text>
|
||||
|
|
@ -252,9 +264,15 @@ function EntryPoints() {
|
|||
<NavButton onClick={() => setOpen(true)}>
|
||||
<NavItemContent>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon src={Icons.Link} size="100" />
|
||||
</Avatar>
|
||||
<NavGutter>
|
||||
<Avatar size="200" radii="400">
|
||||
<Icon
|
||||
src={Icons.Link}
|
||||
size="100"
|
||||
style={{ opacity: NAV_ICON_REST_OPACITY }}
|
||||
/>
|
||||
</Avatar>
|
||||
</NavGutter>
|
||||
<Text as="span" size="Inherit" truncate>
|
||||
Join with Address
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { as, Chip, Icon, Icons, Text } from 'folds';
|
||||
import classNames from 'classnames';
|
||||
import { NavGutter } from '../../components/nav';
|
||||
import * as css from './styles.css';
|
||||
|
||||
export const RoomNavCategoryButton = as<'button', { closed?: boolean }>(
|
||||
|
|
@ -9,17 +10,17 @@ export const RoomNavCategoryButton = as<'button', { closed?: boolean }>(
|
|||
className={classNames(css.CategoryButton, className)}
|
||||
variant="Background"
|
||||
radii="Pill"
|
||||
before={
|
||||
aria-expanded={closed === undefined ? undefined : !closed}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<NavGutter>
|
||||
<Icon
|
||||
className={css.CategoryButtonIcon}
|
||||
size="50"
|
||||
src={closed ? Icons.ChevronRight : Icons.ChevronBottom}
|
||||
/>
|
||||
}
|
||||
aria-expanded={closed === undefined ? undefined : !closed}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
</NavGutter>
|
||||
<Text size="O400" priority="300" truncate>
|
||||
{children}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -20,8 +20,12 @@ import {
|
|||
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 { NavGutter, NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
||||
import {
|
||||
NAV_ICON_REST_OPACITY,
|
||||
NAV_ICON_UNREAD_OPACITY,
|
||||
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';
|
||||
|
|
@ -330,34 +334,36 @@ export function RoomNavItem({
|
|||
<NavLink to={linkPath} onClick={room.isCallRoom() ? handleStartCall : undefined}>
|
||||
<NavItemContent>
|
||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||
<Avatar size="200" radii="400">
|
||||
{showAvatar ? (
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={
|
||||
direct
|
||||
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
}
|
||||
alt={roomName}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(roomName)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<RoomIcon
|
||||
style={{
|
||||
opacity: unread ? config.opacity.P500 : config.opacity.P300,
|
||||
}}
|
||||
filled={selected}
|
||||
size="100"
|
||||
joinRule={room.getJoinRule()}
|
||||
roomType={room.getType()}
|
||||
/>
|
||||
)}
|
||||
</Avatar>
|
||||
<NavGutter>
|
||||
<Avatar size="200" radii="400">
|
||||
{showAvatar ? (
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={
|
||||
direct
|
||||
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
|
||||
}
|
||||
alt={roomName}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(roomName)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<RoomIcon
|
||||
style={{
|
||||
opacity: unread ? NAV_ICON_UNREAD_OPACITY : NAV_ICON_REST_OPACITY,
|
||||
}}
|
||||
filled={selected}
|
||||
size="100"
|
||||
joinRule={room.getJoinRule()}
|
||||
roomType={room.getType()}
|
||||
/>
|
||||
)}
|
||||
</Avatar>
|
||||
</NavGutter>
|
||||
<Box as="span" grow="Yes">
|
||||
<Text
|
||||
className={NavLabel}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,15 @@ import { style } from '@vanilla-extract/css';
|
|||
import { config } from 'folds';
|
||||
import { surfaceInk } from '../scene/sceneShell.css';
|
||||
import { skyLabelInk } from '../scene/skyLabelInk';
|
||||
import { NAV_ICON_REST_OPACITY } from '../../components/nav/styles.css';
|
||||
|
||||
export const CategoryButton = style({
|
||||
flexGrow: 1,
|
||||
gap: config.space.S200,
|
||||
selectors: {
|
||||
// Same left inset as a row's NavItemContent, so the chevron's gutter sits over the rows' gutters.
|
||||
'&&': { paddingLeft: config.space.S200 },
|
||||
},
|
||||
'@media': {
|
||||
// A section heading on a phone is plain text: no pill behind it.
|
||||
'screen and (max-width: 750px)': {
|
||||
|
|
@ -25,5 +31,5 @@ export const CategoryButton = style({
|
|||
},
|
||||
});
|
||||
export const CategoryButtonIcon = style({
|
||||
opacity: config.opacity.P400,
|
||||
opacity: NAV_ICON_REST_OPACITY,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue