mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
fix(opal): Lock the document while the app frame is mounted (#14590)
This commit is contained in:
1 parent
d77e02b6c5
commit
48c2f9779c
7 files changed
+61
-54
No files matched your search
@@ -13,9 +13,15 @@ footer.
|
||||
|
||||
Full-viewport flex row that wraps all other `RootLayout` primitives.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `children` | `ReactNode` | — | `Sidebar`, `LeftPanel`, `App`, `RightPanel` |
|
||||
Mounting it locks the document: `html`/`body` get `height: 100%; overflow: hidden`
|
||||
(via `:has()`), the frame anchors itself at `h-dvh overflow-hidden`, and
|
||||
`MainContent` becomes the app's one scroller. The sidebar and panels are planted —
|
||||
nothing that grows inside a page can scroll them out of view. Surfaces that do not
|
||||
mount the frame (auth, error pages) keep normal document scrolling.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ---------- | ----------- | ------- | ------------------------------------------- |
|
||||
| `children` | `ReactNode` | — | `Sidebar`, `LeftPanel`, `App`, `RightPanel` |
|
||||
|
||||
### Sidebar
|
||||
|
||||
@@ -28,11 +34,11 @@ Controlled sidebar that handles three viewport sizes:
|
||||
backdrop that closes on click. `useSidebarFolded()` always returns `false`
|
||||
here (content is always expanded; the overlay handles visibility).
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `folded` | `boolean` | — | Current fold state — controlled by the consumer |
|
||||
| `onFoldToggle` | `() => void` | — | Called when the sidebar should toggle |
|
||||
| `children` | `ReactNode` | — | Sidebar shell and body content |
|
||||
| Prop | Type | Default | Description |
|
||||
| -------------- | ------------ | ------- | ----------------------------------------------- |
|
||||
| `folded` | `boolean` | — | Current fold state — controlled by the consumer |
|
||||
| `onFoldToggle` | `() => void` | — | Called when the sidebar should toggle |
|
||||
| `children` | `ReactNode` | — | Sidebar shell and body content |
|
||||
|
||||
### App
|
||||
|
||||
@@ -51,10 +57,10 @@ Accepts all standard `div` props.
|
||||
Permanent `shrink-0` columns that push `App` rather than overlaying it.
|
||||
Width is caller-supplied via `className` (e.g. `className="w-80"`).
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | — | Width and any other overrides (twMerge applied) |
|
||||
| `children` | `ReactNode` | — | Panel content |
|
||||
| Prop | Type | Default | Description |
|
||||
| ----------- | ----------- | ------- | ----------------------------------------------- |
|
||||
| `className` | `string` | — | Width and any other overrides (twMerge applied) |
|
||||
| `children` | `ReactNode` | — | Panel content |
|
||||
|
||||
### Header
|
||||
|
||||
@@ -64,10 +70,10 @@ Pinned `shrink-0` top bar inside `App`.
|
||||
|
||||
Pinned `shrink-0` bottom bar inside `App`.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `extraPadding` | `boolean` | `false` | Adds `14 px` top padding for shadow breathing room above the input bar |
|
||||
| `children` | `ReactNode` | — | Footer content |
|
||||
| Prop | Type | Default | Description |
|
||||
| -------------- | ----------- | ------- | ---------------------------------------------------------------------- |
|
||||
| `extraPadding` | `boolean` | `false` | Adds `14 px` top padding for shadow breathing room above the input bar |
|
||||
| `children` | `ReactNode` | — | Footer content |
|
||||
|
||||
## Hooks
|
||||
|
||||
@@ -101,14 +107,12 @@ import { RootLayout } from "@opal/layouts";
|
||||
<RootLayout.Header>
|
||||
<AppHeader />
|
||||
</RootLayout.Header>
|
||||
<RootLayout.MainContent>
|
||||
{children}
|
||||
</RootLayout.MainContent>
|
||||
<RootLayout.MainContent>{children}</RootLayout.MainContent>
|
||||
<RootLayout.Footer>
|
||||
<AppFooter />
|
||||
</RootLayout.Footer>
|
||||
</RootLayout.App>
|
||||
</RootLayout.Root>
|
||||
</RootLayout.Root>;
|
||||
```
|
||||
|
||||
### With panels
|
||||
@@ -122,9 +126,7 @@ import { RootLayout } from "@opal/layouts";
|
||||
<FilterPanel />
|
||||
</RootLayout.LeftPanel>
|
||||
<RootLayout.App>
|
||||
<RootLayout.MainContent>
|
||||
{children}
|
||||
</RootLayout.MainContent>
|
||||
<RootLayout.MainContent>{children}</RootLayout.MainContent>
|
||||
</RootLayout.App>
|
||||
<RootLayout.RightPanel className="w-80">
|
||||
<DetailPanel />
|
||||
@@ -141,8 +143,12 @@ import { RootLayout } from "@opal/layouts";
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
<RootLayout.Header><AppHeader /></RootLayout.Header>
|
||||
<RootLayout.Header>
|
||||
<AppHeader />
|
||||
</RootLayout.Header>
|
||||
<RootLayout.MainContent>{children}</RootLayout.MainContent>
|
||||
<RootLayout.Footer><AppFooter /></RootLayout.Footer>
|
||||
<RootLayout.Footer>
|
||||
<AppFooter />
|
||||
</RootLayout.Footer>
|
||||
</RootLayout.App>
|
||||
```
|
||||
@@ -1,8 +1,27 @@
|
||||
@reference "#reference.css";
|
||||
|
||||
/* Root — full-viewport flex row */
|
||||
/*
|
||||
Scroll contract: when the app frame is mounted, the document never
|
||||
scrolls. The frame is exactly one viewport tall, the sidebar is planted
|
||||
inside it, and `__main` is the one scrollable slot. Scoped with `:has()`
|
||||
so surfaces without the frame (auth, error pages) keep normal document
|
||||
scrolling.
|
||||
|
||||
Without this, `html`/`body` have no height, so any in-flow element that
|
||||
outgrows the viewport scrolls the document and drags the sidebar with it.
|
||||
*/
|
||||
html:has(.opal-root-layout),
|
||||
html:has(.opal-root-layout) body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Root — full-viewport flex row. `h-dvh` (not `h-full`) so its height never
|
||||
depends on the wrapper chain above it; `overflow-hidden` because the frame
|
||||
itself never scrolls. The desktop shell clamps this below its title bar in
|
||||
css/desktop-titlebar.css. */
|
||||
.opal-root-layout {
|
||||
@apply flex flex-row w-full h-full;
|
||||
@apply flex flex-row w-full h-dvh overflow-hidden;
|
||||
}
|
||||
|
||||
/* App — fills remaining flex space; flex-col so Header/Footer pin correctly */
|
||||
@@ -10,9 +29,10 @@
|
||||
@apply flex-1 flex flex-col overflow-hidden h-full w-full;
|
||||
}
|
||||
|
||||
/* Main content — scrollable slot inside App */
|
||||
/* Main content — the one scrollable slot. `min-h-0` so it shrinks to its
|
||||
flex basis instead of being grown past the frame by its content. */
|
||||
.opal-root-layout__main {
|
||||
@apply flex-1 overflow-auto;
|
||||
@apply flex-1 min-h-0 overflow-auto;
|
||||
}
|
||||
|
||||
/* Panel — permanent column (width determined by child content) */
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
SidebarRoot — desktop column
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* `h-full`, not `h-screen`: the root frame owns the viewport height, and a
|
||||
column asserting `100vh` on its own is what let a leaf define the app's
|
||||
geometry (and overflow the frame on mobile, where 100vh > 100dvh). */
|
||||
.opal-sidebar-root__column {
|
||||
@apply h-screen flex flex-col bg-background-tint-02 pb-2 shrink-0;
|
||||
@apply h-full min-h-0 flex flex-col bg-background-tint-02 pb-2 shrink-0;
|
||||
width: var(--sidebar-width-expanded);
|
||||
transition: width 200ms ease-in-out;
|
||||
}
|
||||
|
||||
@@ -129,14 +129,7 @@ export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
|
||||
<PageSelector
|
||||
totalPages={Math.ceil(slackBots.length / NUM_IN_PAGE)}
|
||||
currentPage={page}
|
||||
onPageChange={(newPage) => {
|
||||
setPage(newPage);
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,7 @@ html.onyx-desktop > body {
|
||||
*/
|
||||
html.onyx-desktop .h-screen,
|
||||
html.onyx-desktop .max-h-screen,
|
||||
html.onyx-desktop .opal-root-layout,
|
||||
html.onyx-desktop .opal-sidebar-root__column,
|
||||
html.onyx-desktop .opal-sidebar-root__overlay[data-variant="mobile"],
|
||||
html.onyx-desktop .opal-sidebar-root__overlay[data-variant="small"] {
|
||||
|
||||
@@ -338,7 +338,6 @@ const StandardAnswersTable = ({
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
shouldScroll={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -34,10 +34,6 @@ const getPaginationOptions = (
|
||||
return paginationOptions;
|
||||
};
|
||||
|
||||
const scrollUp = () => {
|
||||
setTimeout(() => window.scrollTo({ top: 0 }), 50);
|
||||
};
|
||||
|
||||
type PageLinkProps = {
|
||||
linkText: string | number;
|
||||
pageChangeHandler?: () => void;
|
||||
@@ -86,21 +82,14 @@ export interface PageSelectorProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (newPage: number) => void;
|
||||
shouldScroll?: boolean;
|
||||
}
|
||||
|
||||
export const PageSelector = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
shouldScroll = false,
|
||||
}: PageSelectorProps) => {
|
||||
const paginationOptions = getPaginationOptions(currentPage, totalPages);
|
||||
const modifiedScrollUp = () => {
|
||||
if (shouldScroll) {
|
||||
scrollUp();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "inline-block" }}>
|
||||
@@ -109,7 +98,6 @@ export const PageSelector = ({
|
||||
unclickable={currentPage === 1}
|
||||
pageChangeHandler={() => {
|
||||
onPageChange(Math.max(currentPage - 1, 1));
|
||||
modifiedScrollUp();
|
||||
}}
|
||||
/>
|
||||
{!paginationOptions.includes(1) && (
|
||||
@@ -119,7 +107,6 @@ export const PageSelector = ({
|
||||
active={currentPage === 1}
|
||||
pageChangeHandler={() => {
|
||||
onPageChange(1);
|
||||
modifiedScrollUp();
|
||||
}}
|
||||
/>
|
||||
<PageLink linkText="..." unclickable={true} />
|
||||
@@ -136,7 +123,6 @@ export const PageSelector = ({
|
||||
linkText={page}
|
||||
pageChangeHandler={() => {
|
||||
onPageChange(page);
|
||||
modifiedScrollUp();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -146,7 +132,6 @@ export const PageSelector = ({
|
||||
unclickable={currentPage === totalPages}
|
||||
pageChangeHandler={() => {
|
||||
onPageChange(Math.min(currentPage + 1, totalPages));
|
||||
modifiedScrollUp();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user