feat: Integrate forum layout support across various components and enhance room handling logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
60
src/app/components/speech-bubble/SpeechBubble.css.ts
Normal file
60
src/app/components/speech-bubble/SpeechBubble.css.ts
Normal 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',
|
||||
});
|
||||
|
||||
43
src/app/components/speech-bubble/SpeechBubble.tsx
Normal file
43
src/app/components/speech-bubble/SpeechBubble.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user