fix(mobile): overflow breaks — tables, composer, call bar, previews, cards (M1)
Mobile-audit batch 1. All fixes reuse cinny's own responsive primitives and are mobile-gated so desktop is unchanged. - Message tables: wrap <table> in an overflow-x container so a wide table scrolls instead of overflowing the message column / page body. - Composer toolbar: let the before|editable|after row and the toolbar wrap on phones (@media <=750px) instead of squeezing the editable to zero and pushing Send off-screen. - In-call control bar: collapse to the compact/stacked layout on a mobile viewport (ScreenSize.Mobile) too, not just when the bar's own container is <500px — fixes the 500-750px band where the control row overflowed. - URL-preview card: base width toRem(400) -> min(25rem, 92vw) so a single card fits a narrow phone (still exactly 400px on desktop). - Explore card grid: drop to one column at <=750px (was a fixed 3-col grid). Two review passes: desktop behavior provably unchanged (all gated by @media / ScreenSize.Mobile; the table wrapper only contains previously-overflowing tables). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,9 +16,23 @@ export const EditorOptions = style([
|
|||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
padding: config.space.S200,
|
padding: config.space.S200,
|
||||||
|
'@media': {
|
||||||
|
// On phones the toolbar can hold many 44px buttons; let them wrap to a
|
||||||
|
// second line instead of overflowing horizontally.
|
||||||
|
'(max-width: 750px)': { flexWrap: 'wrap' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// The composer's before | editable | after row. On phones, allow the toolbar
|
||||||
|
// (`after`) to wrap below the input instead of squeezing the editable to zero
|
||||||
|
// and pushing the Send button off-screen.
|
||||||
|
export const EditorInputRow = style({
|
||||||
|
'@media': {
|
||||||
|
'(max-width: 750px)': { flexWrap: 'wrap' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const EditorTextareaScroll = style({});
|
export const EditorTextareaScroll = style({});
|
||||||
|
|
||||||
export const EditorTextarea = style([
|
export const EditorTextarea = style([
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
|||||||
<div className={css.Editor} ref={ref}>
|
<div className={css.Editor} ref={ref}>
|
||||||
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
|
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
|
||||||
{top}
|
{top}
|
||||||
<Box alignItems="Start">
|
<Box className={css.EditorInputRow} alignItems="Start">
|
||||||
{before && (
|
{before && (
|
||||||
<Box className={css.EditorOptions} alignItems="Center" gap="100" shrink="No">
|
<Box className={css.EditorOptions} alignItems="Center" gap="100" shrink="No">
|
||||||
{before}
|
{before}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export const CardGrid = style({
|
|||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||||
gap: config.space.S400,
|
gap: config.space.S400,
|
||||||
|
'@media': {
|
||||||
|
// Cards squish/overflow below ~360px each; drop to a single column on phones
|
||||||
|
// (mirrors the 750px breakpoint the nav uses).
|
||||||
|
'(max-width: 750px)': { gridTemplateColumns: '1fr' },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RoomCardBase = style([
|
export const RoomCardBase = style([
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { DefaultReset, color, config, toRem } from 'folds';
|
|||||||
export const UrlPreview = style([
|
export const UrlPreview = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
width: toRem(400),
|
// 25rem (=400px) on desktop, but shrink to fit narrow phones so a single
|
||||||
|
// card doesn't exceed the viewport (mirrors UrlPreviewWide's min()).
|
||||||
|
width: 'min(25rem, 92vw)',
|
||||||
minHeight: toRem(102),
|
minHeight: toRem(102),
|
||||||
backgroundColor: color.SurfaceVariant.Container,
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
color: color.SurfaceVariant.OnContainer,
|
color: color.SurfaceVariant.OnContainer,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { useSetting } from '../../state/hooks/settings';
|
|||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { callEmbedAtom } from '../../state/callEmbed';
|
import { callEmbedAtom } from '../../state/callEmbed';
|
||||||
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
||||||
|
import { ScreenSize, useScreenSize } from '../../hooks/useScreenSize';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { useCallEmbedRef } from '../../hooks/useCallEmbed';
|
import { useCallEmbedRef } from '../../hooks/useCallEmbed';
|
||||||
@@ -51,18 +52,25 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
const controlRef = useRef<HTMLDivElement>(null);
|
const controlRef = useRef<HTMLDivElement>(null);
|
||||||
const callEmbedRef = useCallEmbedRef();
|
const callEmbedRef = useCallEmbedRef();
|
||||||
const setCallEmbed = useSetAtom(callEmbedAtom);
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||||
const [compact, setCompact] = useState(document.body.clientWidth < 500);
|
const screenSize = useScreenSize();
|
||||||
|
const [narrowContainer, setNarrowContainer] = useState(document.body.clientWidth < 500);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
useResizeObserver(
|
useResizeObserver(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const element = controlRef.current;
|
const element = controlRef.current;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
setCompact(element.clientWidth < 500);
|
setNarrowContainer(element.clientWidth < 500);
|
||||||
}, []),
|
}, []),
|
||||||
useCallback(() => controlRef.current, []),
|
useCallback(() => controlRef.current, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Collapse to the stacked/compact layout whenever the bar's own container is
|
||||||
|
// narrow (a small desktop call window) OR the viewport is a phone. The old
|
||||||
|
// element-only `< 500` check left the ~11-control row overflowing off-screen
|
||||||
|
// in the 500–750px band (landscape phones / small tablets).
|
||||||
|
const compact = narrowContainer || screenSize === ScreenSize.Mobile;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onFullscreenChange = () => setIsFullscreen(!!document.fullscreenElement);
|
const onFullscreenChange = () => setIsFullscreen(!!document.fullscreenElement);
|
||||||
document.addEventListener('fullscreenchange', onFullscreenChange);
|
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
|||||||
@@ -455,6 +455,17 @@ export const getReactCustomHtmlParser = (
|
|||||||
return <CodeBlock opts={opts}>{children}</CodeBlock>;
|
return <CodeBlock opts={opts}>{children}</CodeBlock>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'table') {
|
||||||
|
// Sanitize allows tables, but a wide one would otherwise overflow the
|
||||||
|
// message column and the page body on narrow screens. Wrap it in a
|
||||||
|
// horizontally-scrollable container (same idea as CodeBlock's Scroll).
|
||||||
|
return (
|
||||||
|
<div style={{ overflowX: 'auto', maxWidth: '100%' }}>
|
||||||
|
<table {...props}>{domToReact(children as unknown as DOMNode[], opts)}</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'blockquote') {
|
if (name === 'blockquote') {
|
||||||
return (
|
return (
|
||||||
<Text {...props} size="Inherit" as="blockquote" className={css.BlockQuote}>
|
<Text {...props} size="Inherit" as="blockquote" className={css.BlockQuote}>
|
||||||
|
|||||||
Reference in New Issue
Block a user