mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 06:15:57 +00:00
Fix vertical mobile overflow
This commit is contained in:
1 parent
44b7059075
commit
325c841e3b
14 files changed
+68
-22
No files matched your search
+25
-3
@@ -833,9 +833,9 @@ dark:bg-black/50`).
|
||||
Every phone bottom sheet has one shape: `bg-card`, 16px top corners, no top
|
||||
border, `max-h-sheet` (the visible viewport less the top safe-area inset and a
|
||||
3rem strip, so the scrim above it can always be tapped to close), and bottom
|
||||
padding that clears the iPhone home indicator. Never cap a sheet with `vh`: on
|
||||
iOS Safari it is the viewport with the toolbars hidden, so a `90vh` sheet slides
|
||||
under the URL bar and covers its scrim. `SheetContent side="bottom"` gives the
|
||||
padding that clears the iPhone home indicator. Never cap a sheet with `vh` (see
|
||||
Viewport heights): a `90vh` sheet slides under Safari's URL bar and covers its
|
||||
scrim. `SheetContent side="bottom"` gives the
|
||||
shape with `pb-safe-0` (the bare inset); pass `handle` for the grab bar, which
|
||||
also hides the X (the overlay dismisses it; the handle is only a cue and doesn't
|
||||
drag; pass `showCloseButton` to keep an X).
|
||||
@@ -916,6 +916,28 @@ values (`h-[calc(100dvh-64px)]`, `max-w-[520px]`) and motion values
|
||||
padding, colours and radii in brackets are not; pick the nearest scale step
|
||||
or add a token.
|
||||
|
||||
### Viewport heights
|
||||
|
||||
Never size anything with `vh` or the `h-screen` family (`h-`, `min-h-`,
|
||||
`max-h-screen`, which are `100vh`). On iOS Safari `vh` is the viewport with the
|
||||
toolbars hidden, so while the URL bar and tab bar show, a `vh` height is about
|
||||
140px taller than the screen: a centred message sits low, a panel or dropdown
|
||||
runs under the tab bar, and the app shell can be dragged and bounce. ESLint
|
||||
(`no-restricted-syntax`) rejects both. Pick by what the height does:
|
||||
|
||||
- **`dvh`** (the visible viewport, follows the toolbars): the app shell
|
||||
(`h-dvh`), full-screen states (`min-h-dvh`: 404, MobileBlocker, the `screen`
|
||||
`LoadingState`), and caps on things that pop up over the page (a search
|
||||
dropdown's `max-h-[calc(100dvh-200px)]`, `Modal`'s `85dvh`).
|
||||
- **`svh`** (the smallest viewport, steady): a fixed-size panel inside a page
|
||||
that scrolls (the Logs panel's `h-[55svh]`, a list's `max-h-[45svh]`, the
|
||||
chunk editor's `min-h`), so it doesn't resize while Safari's bars slide in
|
||||
and out.
|
||||
- **`max-h-sheet`**: every bottom sheet (see Modal, not Dialog).
|
||||
- A row or control has a fixed height (`h-12`), never a share of the viewport.
|
||||
|
||||
`w-screen` is `vw`, which the toolbars don't change, so it stays allowed.
|
||||
|
||||
### Typography roles
|
||||
|
||||
- **Page title**: `SectionPageHeader` (24px bold); the only other text at
|
||||
|
||||
@@ -84,6 +84,28 @@ export default [
|
||||
],
|
||||
},
|
||||
],
|
||||
// On iOS Safari vh (and h-screen, which is 100vh) is the viewport with
|
||||
// the toolbars hidden, so a vh height overflows the visible screen.
|
||||
// w-screen is vw, which the toolbars don't change, so it stays allowed.
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
...[
|
||||
'Literal[value=/\\dvh\\b/]',
|
||||
'TemplateElement[value.raw=/\\dvh\\b/]',
|
||||
].map((selector) => ({
|
||||
selector,
|
||||
message:
|
||||
'vh is the toolbar-hidden viewport on iOS Safari. Use dvh for the app shell and caps on things that pop up, svh for a fixed-size panel in a page that scrolls, or max-h-sheet for a bottom sheet. See DESIGN.md.',
|
||||
})),
|
||||
...[
|
||||
'Literal[value=/(^|[\\s:])(min-|max-)?h-screen\\b/]',
|
||||
'TemplateElement[value.raw=/(^|[\\s:])(min-|max-)?h-screen\\b/]',
|
||||
].map((selector) => ({
|
||||
selector,
|
||||
message:
|
||||
"Tailwind's screen heights (h-/min-h-/max-h-screen) are the toolbar-hidden viewport on iOS Safari, taller than the visible screen. Use h-dvh / min-h-dvh, or svh for a fixed panel. See DESIGN.md.",
|
||||
})),
|
||||
],
|
||||
// Design-system rules (@shadcn/lint). Tokens, variants and the
|
||||
// approved exceptions are documented in DESIGN.md.
|
||||
'shadcn/no-restyle': [
|
||||
|
||||
@@ -64,7 +64,7 @@ function AuthWrapper({ children }: { children: React.ReactNode }) {
|
||||
? t('auth.accountDisabled')
|
||||
: t('auth.signInToContinue');
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-6">
|
||||
<div className="flex h-dvh flex-col items-center justify-center gap-6">
|
||||
<img
|
||||
src={isDarkTheme ? DocsGPTMarkWhite : DocsGPTMark}
|
||||
alt="DocsGPT"
|
||||
@@ -105,12 +105,12 @@ function MainLayout() {
|
||||
|
||||
return (
|
||||
<SidebarLevelProvider>
|
||||
<div className="bg-background relative h-screen overflow-hidden">
|
||||
<div className="bg-background relative h-dvh overflow-hidden">
|
||||
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||
<ActionButtons showShare={!inSection} />
|
||||
<div
|
||||
className={cn(
|
||||
'h-[calc(100dvh-56px)] overflow-auto transition-[margin] duration-300 ease-in-out lg:h-screen',
|
||||
'h-[calc(100dvh-56px)] overflow-auto transition-[margin] duration-300 ease-in-out lg:h-dvh',
|
||||
!isMobile ? (navOpen ? 'lg:ml-72' : 'lg:ml-14') : 'ml-0 lg:ml-16',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -489,7 +489,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'visible mt-2 flex h-[6vh] w-full items-center justify-between gap-1 px-2 md:h-12'
|
||||
'visible mt-2 flex h-12 w-full items-center justify-between gap-1 px-2'
|
||||
}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@@ -5,7 +5,7 @@ export default function PageNotFound() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="bg-background grid min-h-screen">
|
||||
<div className="bg-background grid min-h-dvh">
|
||||
<div className="text-foreground bg-muted mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl p-6 lg:p-10 xl:p-16">
|
||||
<h1 className="text-3xl">{t('pageNotFound.title')}</h1>
|
||||
<p>{t('pageNotFound.message')}</p>
|
||||
|
||||
@@ -251,7 +251,7 @@ export default function GuardrailEvents({ agentId }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="border-border bg-card mt-3 w-full overflow-hidden rounded-xl border">
|
||||
<div className="max-h-[45vh] overflow-y-auto">
|
||||
<div className="max-h-[45svh] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="p-3">
|
||||
<SkeletonLoader count={3} />
|
||||
|
||||
@@ -1778,7 +1778,7 @@ function WorkflowBuilderInner() {
|
||||
return (
|
||||
<>
|
||||
<MobileBlocker />
|
||||
<div className="bg-background fixed inset-0 z-50 hidden h-screen w-full flex-col lg:flex">
|
||||
<div className="bg-background fixed inset-0 z-50 hidden h-dvh w-full flex-col lg:flex">
|
||||
<div className="border-border bg-card flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{canManageAgent ? (
|
||||
@@ -2136,7 +2136,7 @@ function WorkflowBuilderInner() {
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[calc(100vh-200px)] overflow-y-auto p-4">
|
||||
<div className="max-h-[calc(100dvh-200px)] overflow-y-auto p-4">
|
||||
<div className="mb-4 flex flex-col gap-5">
|
||||
<div className="bg-muted rounded-lg p-3">
|
||||
<div className="text-muted-foreground mb-1 text-xs">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
export default function MobileBlocker() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="bg-background flex min-h-screen flex-col items-center justify-center px-6 text-center lg:hidden">
|
||||
<div className="bg-background flex min-h-dvh flex-col items-center justify-center px-6 text-center lg:hidden">
|
||||
<div className="bg-secondary mb-6 flex size-20 items-center justify-center rounded-2xl">
|
||||
<Monitor className="text-primary size-10" />
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,9 @@ const LineNumberedTextarea: React.FC<LineNumberedTextareaProps> = ({
|
||||
<textarea
|
||||
className={cn(
|
||||
'text-foreground focus-visible:ring-ring/50 focus-visible:border-ring h-(--numbered-height) w-full resize-none overflow-hidden border-none bg-transparent pl-8 text-sm leading-5 outline-none focus-visible:ring-3 lg:pl-12',
|
||||
isMobile ? 'min-h-[calc(100vh-200px)]' : 'min-h-[calc(100vh-300px)]',
|
||||
isMobile
|
||||
? 'min-h-[calc(100svh-200px)]'
|
||||
: 'min-h-[calc(100svh-300px)]',
|
||||
!editable && 'select-none',
|
||||
)}
|
||||
value={value}
|
||||
@@ -511,7 +513,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
|
||||
{fileSearchQuery && (
|
||||
<div className="border-border bg-popover text-popover-foreground absolute top-full right-0 left-0 z-20 mt-1 w-full overflow-hidden rounded-xl border shadow-md">
|
||||
<CommandList className="max-h-[calc(100vh-200px)]">
|
||||
<CommandList className="max-h-[calc(100dvh-200px)]">
|
||||
{fileSearchResults.length === 0 ? (
|
||||
<CommandEmpty>{t('settings.sources.noResults')}</CommandEmpty>
|
||||
) : (
|
||||
@@ -603,7 +605,7 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
<EmptyState
|
||||
size="sm"
|
||||
title={t('settings.sources.noChunks')}
|
||||
className="col-span-full min-h-[50vh] w-full"
|
||||
className="col-span-full min-h-[50svh] w-full"
|
||||
/>
|
||||
) : (
|
||||
filteredChunks.map((chunk, index) => (
|
||||
|
||||
@@ -240,7 +240,7 @@ export function MultiSelectPopover({
|
||||
align={align}
|
||||
side={side}
|
||||
className={cn(
|
||||
'flex max-h-[min(600px,80vh)] w-[min(462px,calc(100vw-20px))] flex-col overflow-hidden p-0',
|
||||
'flex max-h-[min(600px,80dvh)] w-[min(462px,calc(100vw-20px))] flex-col overflow-hidden p-0',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -593,7 +593,7 @@ const TreeBrowser: React.FC<TreeBrowserProps> = ({
|
||||
|
||||
{searchQuery && (
|
||||
<div className="border-border bg-popover text-popover-foreground absolute top-full right-0 left-0 z-20 mt-1 w-full overflow-hidden rounded-xl border shadow-md">
|
||||
<CommandList className="max-h-[calc(100vh-200px)] overscroll-contain">
|
||||
<CommandList className="max-h-[calc(100dvh-200px)] overscroll-contain">
|
||||
{searchResults.length === 0 ? (
|
||||
<CommandEmpty>{t('settings.sources.noResults')}</CommandEmpty>
|
||||
) : (
|
||||
|
||||
@@ -15,12 +15,12 @@ describe('LoadingState', () => {
|
||||
expect(html.match(/role="status"/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('uses py-10 for block and h-screen for screen', () => {
|
||||
it('uses py-10 for block and the visible viewport (h-dvh) for screen', () => {
|
||||
expect(renderToStaticMarkup(<LoadingState fill="block" />)).toContain(
|
||||
'py-10',
|
||||
);
|
||||
expect(renderToStaticMarkup(<LoadingState fill="screen" />)).toContain(
|
||||
'h-screen',
|
||||
'h-dvh',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Spinner, type SpinnerProps } from './spinner';
|
||||
// section with none takes `block`.
|
||||
const FILL_CLASSES = {
|
||||
parent: 'h-full',
|
||||
screen: 'h-screen',
|
||||
screen: 'h-dvh',
|
||||
block: 'py-10',
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -250,13 +250,13 @@ function LogsTable({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="border-border bg-card h-[55vh] w-full overflow-hidden rounded-xl border font-mono">
|
||||
<div className="border-border bg-card h-[55svh] w-full overflow-hidden rounded-xl border font-mono">
|
||||
<div className="bg-muted flex h-8 flex-col items-start justify-center">
|
||||
<p className="text-muted-foreground px-3 text-xs">
|
||||
{tableHeader ? tableHeader : t('settings.logs.tableHeader')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative flex h-[51vh] grow flex-col items-start gap-2 overflow-y-auto overscroll-contain bg-transparent p-4">
|
||||
<div className="relative flex h-[51svh] grow flex-col items-start gap-2 overflow-y-auto overscroll-contain bg-transparent p-4">
|
||||
{!loading && logs.length === 0 && (
|
||||
<EmptyState
|
||||
size="xs"
|
||||
|
||||
Reference in new issue
Block a user