feat: Integrate forum layout support across various components and enhance room handling logic

This commit is contained in:
2026-07-06 01:06:20 +10:00
parent d1626e3585
commit c57797ffe4
62 changed files with 6876 additions and 385 deletions

View File

@@ -85,6 +85,8 @@ type CustomEditorProps = {
before?: ReactNode;
after?: ReactNode;
maxHeight?: string;
/** Stretch the editable area to fill the parent (e.g. post modal). */
fillHeight?: boolean;
editor: Editor;
placeholder?: string;
onKeyDown?: KeyboardEventHandler;
@@ -101,6 +103,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
before,
after,
maxHeight = '50vh',
fillHeight = false,
editor,
placeholder,
onKeyDown,
@@ -223,11 +226,27 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
[]
);
const scrollStyle = fillHeight
? { flex: '1 1 auto', minHeight: maxHeight, height: '100%' }
: { maxHeight };
return (
<div className={css.Editor} ref={ref}>
<div
className={css.Editor}
ref={ref}
style={
fillHeight
? { display: 'flex', flexDirection: 'column', height: '100%', minHeight: maxHeight }
: undefined
}
>
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
{top}
<Box alignItems="Start">
<Box
alignItems="Start"
grow={fillHeight ? 'Yes' : undefined}
style={fillHeight ? { width: '100%', minHeight: 0, flex: '1 1 auto' } : undefined}
>
{before && (
<Box className={css.EditorOptions} alignItems="Center" gap="100" shrink="No">
{before}
@@ -236,7 +255,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
<Scroll
className={css.EditorTextareaScroll}
variant="SurfaceVariant"
style={{ maxHeight }}
style={scrollStyle}
size="300"
visibility="Hover"
hideTrack

View File

@@ -28,14 +28,18 @@ export function PageRoot({ nav, children }: PageRootProps) {
type ClientDrawerLayoutProps = {
children: ReactNode;
};
export function PageNav({ size, children }: ClientDrawerLayoutProps & css.PageNavVariants) {
export function PageNav({
size,
children,
className,
}: ClientDrawerLayoutProps & css.PageNavVariants & { className?: string }) {
const screenSize = useScreenSizeContext();
const isMobile = screenSize === ScreenSize.Mobile;
return (
<Box
grow={isMobile ? 'Yes' : undefined}
className={css.PageNav({ size })}
className={classNames(css.PageNav({ size }), className)}
shrink={isMobile ? 'Yes' : 'No'}
>
<Box grow="Yes" direction="Column">

View File

@@ -166,6 +166,7 @@ export const RoomCard = as<'div', RoomCardProps>(
const useAuthentication = useMediaAuthentication();
const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias);
const joinedRoom = mx.getRoom(joinedRoomId);
const createEvent = joinedRoom ? getStateEvent(joinedRoom, StateEvent.RoomCreate) : undefined;
const [topicEvent, setTopicEvent] = useState(() =>
joinedRoom ? getStateEvent(joinedRoom, StateEvent.RoomTopic) : undefined
);
@@ -210,6 +211,8 @@ export const RoomCard = as<'div', RoomCardProps>(
const [showSpaceJoinPrompt, setShowSpaceJoinPrompt] = useState(false);
const isSpace = roomType === RoomType.Space || joinedRoom?.isSpaceRoom();
const isForum =
roomType === RoomType.Forum || createEvent?.getContent<{ type?: string }>().type === RoomType.Forum;
const handleJoinClick = () => {
if (isSpace) {
@@ -252,6 +255,11 @@ export const RoomCard = as<'div', RoomCardProps>(
<Text size="L400">Space</Text>
</Badge>
)}
{isForum && (
<Badge variant="Secondary" fill="Soft" outlined>
<Text size="L400">Forum</Text>
</Badge>
)}
</Box>
<Box grow="Yes" direction="Column" gap="100">
<RoomCardName>{roomName}</RoomCardName>

View File

@@ -0,0 +1,60 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
const tailTopBase = {
content: '""',
position: 'absolute' as const,
top: toRem(-5),
left: 'var(--speech-tail-x, 50%)',
transform: 'translateX(-50%) rotate(45deg)',
width: toRem(10),
height: toRem(10),
};
export const SpeechBubble = style({
position: 'relative',
padding: `${toRem(8)} ${toRem(12)}`,
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
borderRadius: config.radii.R300,
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
});
export const SpeechBubbleTailTop = style({
selectors: {
'&::before': {
...tailTopBase,
backgroundColor: color.SurfaceVariant.Container,
},
},
});
/** Popover menus (kebab, etc.) — surface matches folds Menu. */
export const SpeechBubbleMenu = style({
position: 'relative',
padding: 0,
backgroundColor: color.Surface.Container,
color: color.Surface.OnContainer,
borderRadius: config.radii.R400,
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
boxShadow: '0 10px 28px rgb(0 0 0 / 28%), 0 2px 6px rgb(0 0 0 / 12%)',
overflow: 'visible',
});
export const SpeechBubbleMenuTailTop = style({
selectors: {
'&::before': {
...tailTopBase,
backgroundColor: color.Surface.Container,
borderLeft: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
borderTop: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
},
},
});
globalStyle(`${SpeechBubbleMenu} > *`, {
background: 'transparent',
border: 'none',
boxShadow: 'none',
});

View File

@@ -0,0 +1,43 @@
import React from 'react';
import classNames from 'classnames';
import * as css from './SpeechBubble.css';
type SpeechBubbleProps = {
children: React.ReactNode;
className?: string;
/** `menu` for dropdowns; `tooltip` for hover cards. */
variant?: 'tooltip' | 'menu';
/** Tail position. Currently only 'top' is implemented. */
tail?: 'top' | 'none';
/** Percentage string, e.g. '50%'. */
tailX?: string;
style?: React.CSSProperties;
};
export function SpeechBubble({
children,
className,
variant = 'tooltip',
tail = 'top',
tailX,
style,
}: SpeechBubbleProps) {
const isMenu = variant === 'menu';
return (
<div
className={classNames(
isMenu ? css.SpeechBubbleMenu : css.SpeechBubble,
tail === 'top' ? (isMenu ? css.SpeechBubbleMenuTailTop : css.SpeechBubbleTailTop) : undefined,
className
)}
style={{
...(tailX ? ({ ['--speech-tail-x' as string]: tailX } as React.CSSProperties) : null),
...style,
}}
>
{children}
</div>
);
}