From 13c5cfdc93038778ee68873d50391039675e46b7 Mon Sep 17 00:00:00 2001 From: Sandeep Prajapati <278588035+sandy-yield@users.noreply.github.com> Date: Thu, 25 Jun 2026 01:00:10 +0530 Subject: [PATCH 1/4] feat(widget): add collapsible split view for responsive dashboard Collapse the dashboard's two-column layout into a single panel at/below 800px, with an edge toggle bar (sitting on the hidden side) that swaps between panels while keeping both mounted to preserve state. Adds a useMediaQuery hook and shared split breakpoint helpers, scopes the column max-widths to desktop, and animates the panel transition. --- packages/widget/src/hooks/use-media-query.ts | 29 +++++ .../common/components/split-view/index.tsx | 91 +++++++++++++ .../components/split-view/styles.css.ts | 122 ++++++++++++++++++ .../common/components/styles.css.ts | 14 +- .../common/components/tab-page-container.tsx | 24 ++-- .../src/pages-dashboard/overview/index.tsx | 51 ++++---- .../pages-dashboard/overview/styles.css.ts | 7 +- .../position-details/index.tsx | 71 +++++----- .../position-details/styles.css.ts | 13 +- packages/widget/src/style.css.ts | 1 + .../widget/src/styles/theme/global.css.ts | 6 + .../widget/src/styles/tokens/breakpoints.ts | 20 ++- .../src/translation/English/translations.json | 5 + .../src/translation/French/translations.json | 5 + 14 files changed, 374 insertions(+), 85 deletions(-) create mode 100644 packages/widget/src/hooks/use-media-query.ts create mode 100644 packages/widget/src/pages-dashboard/common/components/split-view/index.tsx create mode 100644 packages/widget/src/pages-dashboard/common/components/split-view/styles.css.ts diff --git a/packages/widget/src/hooks/use-media-query.ts b/packages/widget/src/hooks/use-media-query.ts new file mode 100644 index 000000000..ebb20b1c5 --- /dev/null +++ b/packages/widget/src/hooks/use-media-query.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; + +const getMatches = (query: string) => { + if (typeof window === "undefined" || !window.matchMedia) { + return false; + } + + return window.matchMedia(query).matches; +}; + +export const useMediaQuery = (query: string) => { + const [matches, setMatches] = useState(() => getMatches(query)); + + useEffect(() => { + if (typeof window === "undefined" || !window.matchMedia) { + return; + } + + const mediaQueryList = window.matchMedia(query); + const onChange = () => setMatches(mediaQueryList.matches); + + onChange(); + mediaQueryList.addEventListener("change", onChange); + + return () => mediaQueryList.removeEventListener("change", onChange); + }, [query]); + + return matches; +}; diff --git a/packages/widget/src/pages-dashboard/common/components/split-view/index.tsx b/packages/widget/src/pages-dashboard/common/components/split-view/index.tsx new file mode 100644 index 000000000..5255cc0c0 --- /dev/null +++ b/packages/widget/src/pages-dashboard/common/components/split-view/index.tsx @@ -0,0 +1,91 @@ +import clsx from "clsx"; +import { type ReactNode, useState } from "react"; +import { Box } from "../../../../components/atoms/box"; +import { CaretLeftIcon } from "../../../../components/atoms/icons/caret-left"; +import { useMediaQuery } from "../../../../hooks/use-media-query"; +import { splitCollapsedMediaQuery } from "../../../../styles/tokens/breakpoints"; +import { VerticalDivider } from "../divider"; +import * as styles from "./styles.css"; + +type Side = "primary" | "secondary"; + +type SplitViewProps = { + primary: ReactNode; + secondary: ReactNode; + primaryBarLabel: string; + secondaryBarLabel: string; +}; + +export const SplitView = ({ + primary, + secondary, + primaryBarLabel, + secondaryBarLabel, +}: SplitViewProps) => { + const isCollapsed = useMediaQuery(splitCollapsedMediaQuery); + const [activeSide, setActiveSide] = useState("primary"); + + if (!primary || !secondary) { + return {primary || secondary}; + } + + const showPrimary = !isCollapsed || activeSide === "primary"; + const showSecondary = !isCollapsed || activeSide === "secondary"; + const revealLabel = + activeSide === "primary" ? secondaryBarLabel : primaryBarLabel; + + const primaryClass = !isCollapsed + ? styles.panelWrapContents + : showPrimary + ? styles.panelWrapActiveFromLeft + : styles.panelWrapHidden; + + const secondaryClass = !isCollapsed + ? styles.panelWrapContents + : showSecondary + ? styles.panelWrapActiveFromRight + : styles.panelWrapHidden; + + return ( + + {primary} + + {isCollapsed ? ( + + setActiveSide((side) => + side === "primary" ? "secondary" : "primary" + ) + } + > + + + + + + {revealLabel} + + + ) : ( + + )} + + {secondary} + + ); +}; diff --git a/packages/widget/src/pages-dashboard/common/components/split-view/styles.css.ts b/packages/widget/src/pages-dashboard/common/components/split-view/styles.css.ts new file mode 100644 index 000000000..26b2aa272 --- /dev/null +++ b/packages/widget/src/pages-dashboard/common/components/split-view/styles.css.ts @@ -0,0 +1,122 @@ +import { keyframes, style } from "@vanilla-extract/css"; +import { atoms } from "../../../../styles/theme/atoms.css"; +import { vars } from "../../../../styles/theme/contract.css"; +import { OUTLET_PADDING } from "../styles.css"; + +export const container = style({ + alignItems: "stretch", + display: "flex", + flexDirection: "row", + gap: "24px", + justifyContent: "center", + minWidth: 0, + width: "100%", +}); + +export const panelWrapContents = style({ + display: "contents", +}); + +export const panelWrapHidden = style({ + display: "none", +}); + +const reduceMotion = "(prefers-reduced-motion: reduce)"; + +const slideInFromLeft = keyframes({ + from: { opacity: 0, transform: "translateX(-16px)" }, + to: { opacity: 1, transform: "translateX(0)" }, +}); + +const slideInFromRight = keyframes({ + from: { opacity: 0, transform: "translateX(16px)" }, + to: { opacity: 1, transform: "translateX(0)" }, +}); + +const panelWrapActiveBase = style({ + display: "flex", + flex: 1, + flexDirection: "row", + minWidth: 0, +}); + +export const panelWrapActiveFromLeft = style([ + panelWrapActiveBase, + { + animation: `${slideInFromLeft} 260ms ease`, + "@media": { + [reduceMotion]: { animation: "none" }, + }, + }, +]); + +export const panelWrapActiveFromRight = style([ + panelWrapActiveBase, + { + animation: `${slideInFromRight} 260ms ease`, + "@media": { + [reduceMotion]: { animation: "none" }, + }, + }, +]); + +export const bar = style([ + atoms({ + background: "stakeSectionBackground", + }), + { + alignItems: "center", + alignSelf: "stretch", + border: 0, + borderRadius: "10px", + cursor: "pointer", + display: "flex", + flexDirection: "column", + flexShrink: 0, + font: "inherit", + gap: "8px", + justifyContent: "center", + padding: "12px 0", + transition: "background 150ms ease", + width: "30px", + selectors: { + "&:hover": { + background: vars.color.backgroundMuted, + }, + }, + }, +]); + +/** + * Bleed the bar outward into the dashboard outlet padding, leaving an 8px gap + * to the card edge on whichever side it currently lives. + */ +export const barBleedRight = style({ + marginRight: `calc(8px - ${OUTLET_PADDING})`, +}); + +export const barBleedLeft = style({ + marginLeft: `calc(8px - ${OUTLET_PADDING})`, +}); + +export const barIcon = style({ + alignItems: "center", + display: "flex", + justifyContent: "center", + transition: "transform 200ms ease", + "@media": { + [reduceMotion]: { transition: "none" }, + }, +}); + +export const barIconFlipped = style({ + transform: "rotate(180deg)", +}); + +export const barLabel = style({ + fontSize: "13px", + fontWeight: 600, + letterSpacing: "0.02em", + whiteSpace: "nowrap", + writingMode: "vertical-rl", +}); diff --git a/packages/widget/src/pages-dashboard/common/components/styles.css.ts b/packages/widget/src/pages-dashboard/common/components/styles.css.ts index bf83fa854..386c1d9b5 100644 --- a/packages/widget/src/pages-dashboard/common/components/styles.css.ts +++ b/packages/widget/src/pages-dashboard/common/components/styles.css.ts @@ -11,7 +11,9 @@ export const wrapper = recipe({ borderWidth: "1px", borderStyle: "solid", boxShadow: "0px 15px 40px 0px #0000000D", - width: "1000px", + maxWidth: "1000px", + width: "100%", + minWidth: "100%", }, ], variants: { @@ -59,16 +61,6 @@ export const outletWrapper = recipe({ }, }); -export const tabPageContainer = recipe({ - base: { - display: "flex", - flexDirection: "row", - gap: "24px", - alignItems: "stretch", - justifyContent: "center", - }, -}); - export const tabPageDivider = recipe({ base: { alignSelf: "stretch", diff --git a/packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx b/packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx index d0e4d84ea..eb62cbaca 100644 --- a/packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx +++ b/packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx @@ -1,13 +1,13 @@ -import clsx from "clsx"; -import { Box } from "../../../components/atoms/box"; -import { tabPageContainer } from "./styles.css"; +// import clsx from "clsx"; +// import { Box } from "../../../components/atoms/box"; +// import { tabPageContainer } from "./styles.css"; -export const TabPageContainer = ({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) => { - return {children}; -}; +// export const TabPageContainer = ({ +// children, +// className, +// }: { +// children: React.ReactNode; +// className?: string; +// }) => { +// return {children}; +// }; diff --git a/packages/widget/src/pages-dashboard/overview/index.tsx b/packages/widget/src/pages-dashboard/overview/index.tsx index 85a4edac7..b91fceba6 100644 --- a/packages/widget/src/pages-dashboard/overview/index.tsx +++ b/packages/widget/src/pages-dashboard/overview/index.tsx @@ -1,38 +1,43 @@ +import { useTranslation } from "react-i18next"; import { Outlet } from "react-router"; import { Box } from "../../components/atoms/box"; import { AnimationPage } from "../../navigation/containers/animation-page"; import { BackButtonProvider } from "../common/components/back-button"; -import { VerticalDivider } from "../common/components/divider"; -import { TabPageContainer } from "../common/components/tab-page-container"; +import { SplitView } from "../common/components/split-view"; import { EarnDetails } from "./earn-details"; import { earnDetailsWrapper } from "./earn-details/styles.css"; import { overviewPageContainer } from "./styles.css"; export const OverviewPage = () => { + const { t } = useTranslation(); + return ( - - - - - - - - - - - - - + + + + + + } + secondary={ + + + + } + /> ); diff --git a/packages/widget/src/pages-dashboard/overview/styles.css.ts b/packages/widget/src/pages-dashboard/overview/styles.css.ts index 7b03c1d32..e9463f858 100644 --- a/packages/widget/src/pages-dashboard/overview/styles.css.ts +++ b/packages/widget/src/pages-dashboard/overview/styles.css.ts @@ -1,5 +1,10 @@ import { style } from "@vanilla-extract/css"; +import { splitExpandedMediaQuery } from "../../styles/tokens/breakpoints"; export const overviewPageContainer = style({ - maxWidth: "380px", + "@media": { + [splitExpandedMediaQuery]: { + maxWidth: "380px", + }, + }, }); diff --git a/packages/widget/src/pages-dashboard/position-details/index.tsx b/packages/widget/src/pages-dashboard/position-details/index.tsx index 12d622481..df734cf19 100644 --- a/packages/widget/src/pages-dashboard/position-details/index.tsx +++ b/packages/widget/src/pages-dashboard/position-details/index.tsx @@ -9,8 +9,7 @@ import { BackButton, BackButtonProvider, } from "../common/components/back-button"; -import { VerticalDivider } from "../common/components/divider"; -import { TabPageContainer } from "../common/components/tab-page-container"; +import { SplitView } from "../common/components/split-view"; import { positionDetailsActionsHasContent, positionDetailsStakeHasContent, @@ -53,6 +52,7 @@ const PositionBreadcrumb = ({ }; const PositionDetailsPageComponent = () => { + const { t } = useTranslation(); const positionDetails = usePositionDetails(); const shouldShowActions = positionDetailsActionsHasContent(positionDetails) || @@ -64,45 +64,48 @@ const PositionDetailsPageComponent = () => { return ( - - {shouldShowActions ? ( + + + + + + + + ) : null + } + secondary={ - + {shouldShowActions ? null : ( + + )} - - - + - ) : null} - - {shouldShowActions ? : null} - - - {shouldShowActions ? null : ( - - )} - - - - + } + /> ); }; diff --git a/packages/widget/src/pages-dashboard/position-details/styles.css.ts b/packages/widget/src/pages-dashboard/position-details/styles.css.ts index b4d05886f..82529343a 100644 --- a/packages/widget/src/pages-dashboard/position-details/styles.css.ts +++ b/packages/widget/src/pages-dashboard/position-details/styles.css.ts @@ -1,5 +1,6 @@ import { style } from "@vanilla-extract/css"; import { atoms } from "../../styles/theme/atoms.css"; +import { splitExpandedMediaQuery } from "../../styles/tokens/breakpoints"; export const posistionDetailsInfoContainer = style([ atoms({ @@ -8,12 +9,20 @@ export const posistionDetailsInfoContainer = style([ width: "0", }), { - maxWidth: "600px", + "@media": { + [splitExpandedMediaQuery]: { + maxWidth: "600px", + }, + }, }, ]); export const positionDetailsActionsContainer = style({ - maxWidth: "380px", + "@media": { + [splitExpandedMediaQuery]: { + maxWidth: "380px", + }, + }, }); export const breadcrumb = style([ diff --git a/packages/widget/src/style.css.ts b/packages/widget/src/style.css.ts index 42be2717b..e3b83226c 100644 --- a/packages/widget/src/style.css.ts +++ b/packages/widget/src/style.css.ts @@ -43,6 +43,7 @@ export const container = style([ export const appContainer = recipe({ base: { minHeight: "800px", + padding: "16px", containerType: "inline-size", containerName: appContainerName, }, diff --git a/packages/widget/src/styles/theme/global.css.ts b/packages/widget/src/styles/theme/global.css.ts index 23b2b73a6..387edd5f7 100644 --- a/packages/widget/src/styles/theme/global.css.ts +++ b/packages/widget/src/styles/theme/global.css.ts @@ -9,6 +9,12 @@ globalStyle(rootSelector, { [reset]: { fontFamily: vars.font.body, }, + widget: { + marginLeft: "auto", + marginRight: "auto", + maxWidth: "1000px", + width: "100%", + }, }, }); diff --git a/packages/widget/src/styles/tokens/breakpoints.ts b/packages/widget/src/styles/tokens/breakpoints.ts index 124690d41..83ed52aab 100644 --- a/packages/widget/src/styles/tokens/breakpoints.ts +++ b/packages/widget/src/styles/tokens/breakpoints.ts @@ -6,8 +6,24 @@ export const breakpoints = { export type Breakpoint = keyof typeof breakpoints; -export const minMediaQuery = (breakpoint: Breakpoint) => - `screen and (min-width: ${breakpoints[breakpoint]}px)`; +const SPLIT_COLLAPSE_BREAKPOINT = 800; + +const toPx = (breakpoint: Breakpoint | number) => + typeof breakpoint === "number" ? breakpoint : breakpoints[breakpoint]; + +export const minMediaQuery = (breakpoint: Breakpoint | number) => + `screen and (min-width: ${toPx(breakpoint)}px)`; + +const maxMediaQuery = (breakpoint: Breakpoint | number) => + `screen and (max-width: ${toPx(breakpoint)}px)`; + +export const splitCollapsedMediaQuery = maxMediaQuery( + SPLIT_COLLAPSE_BREAKPOINT +); + +export const splitExpandedMediaQuery = minMediaQuery( + SPLIT_COLLAPSE_BREAKPOINT + 1 +); export const minContainerWidth = ( containerName: string, diff --git a/packages/widget/src/translation/English/translations.json b/packages/widget/src/translation/English/translations.json index cac12c715..a0cf90cd8 100644 --- a/packages/widget/src/translation/English/translations.json +++ b/packages/widget/src/translation/English/translations.json @@ -239,6 +239,11 @@ } }, "dashboard": { + "split_view": { + "earn": "Earn", + "details": "Details", + "actions": "Actions" + }, "details": { "tabs": { "earn": "Earn", diff --git a/packages/widget/src/translation/French/translations.json b/packages/widget/src/translation/French/translations.json index a508d49a0..5be0f9ee0 100644 --- a/packages/widget/src/translation/French/translations.json +++ b/packages/widget/src/translation/French/translations.json @@ -682,6 +682,11 @@ }, "chain_modal_disclaimer": "Alimenté par Yield.xyz", "dashboard": { + "split_view": { + "earn": "Earn", + "details": "Détails", + "actions": "Actions" + }, "details": { "tabs": { "earn": "Earn", From ad9dba1ffd09f3816e578f722a652c99d8c29e96 Mon Sep 17 00:00:00 2001 From: Sandeep Prajapati <278588035+sandy-yield@users.noreply.github.com> Date: Thu, 25 Jun 2026 01:12:45 +0530 Subject: [PATCH 2/4] fix: removed unwanted file --- .../common/components/tab-page-container.tsx | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx diff --git a/packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx b/packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx deleted file mode 100644 index eb62cbaca..000000000 --- a/packages/widget/src/pages-dashboard/common/components/tab-page-container.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// import clsx from "clsx"; -// import { Box } from "../../../components/atoms/box"; -// import { tabPageContainer } from "./styles.css"; - -// export const TabPageContainer = ({ -// children, -// className, -// }: { -// children: React.ReactNode; -// className?: string; -// }) => { -// return {children}; -// }; From a498321647ff35d92f67763ad687793eb177ab80 Mon Sep 17 00:00:00 2001 From: Sandeep Prajapati <278588035+sandy-yield@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:34:54 +0530 Subject: [PATCH 3/4] fix(widget): scope dashboard layout sizing to dashboard variant Restrict the responsive split-view layout styles to dashboard mode so standalone widget integrations render identically to before. Move the root max-width/centering out of the global root selector into a dashboard-only rule gated by a data-sk-layout marker, and drop the appContainer base padding. --- packages/widget/src/App.tsx | 1 + packages/widget/src/style.css.ts | 1 - packages/widget/src/styles/theme/global.css.ts | 15 ++++++++------- packages/widget/src/styles/theme/ids.ts | 3 +++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/widget/src/App.tsx b/packages/widget/src/App.tsx index 8bc4be7c1..39c123d1d 100644 --- a/packages/widget/src/App.tsx +++ b/packages/widget/src/App.tsx @@ -47,6 +47,7 @@ export const SKApp = (props: SKAppProps) => { return ( Date: Wed, 29 Jul 2026 19:10:14 +0530 Subject: [PATCH 4/4] feat(widget): implement app container provider for responsive layout --- packages/widget/src/App.tsx | 12 ++-- .../components/atoms/virtual-list/index.tsx | 2 +- packages/widget/src/hooks/use-media-query.ts | 29 --------- .../widget/src/hooks/use-split-collapsed.ts | 13 ++++ .../common/components/split-view/index.tsx | 5 +- .../pages-dashboard/overview/styles.css.ts | 6 +- .../position-details/styles.css.ts | 10 +-- .../src/providers/app-container/index.tsx | 62 +++++++++++++++++++ packages/widget/src/style.css.ts | 8 ++- .../widget/src/styles/tokens/breakpoints.ts | 21 +++---- .../src/styles/tokens/containers.css.ts | 5 ++ 11 files changed, 110 insertions(+), 63 deletions(-) delete mode 100644 packages/widget/src/hooks/use-media-query.ts create mode 100644 packages/widget/src/hooks/use-split-collapsed.ts create mode 100644 packages/widget/src/providers/app-container/index.tsx create mode 100644 packages/widget/src/styles/tokens/containers.css.ts diff --git a/packages/widget/src/App.tsx b/packages/widget/src/App.tsx index 39c123d1d..2cb70ce03 100644 --- a/packages/widget/src/App.tsx +++ b/packages/widget/src/App.tsx @@ -7,12 +7,11 @@ import { createRef, useImperativeHandle, useState } from "react"; import ReactDOM from "react-dom/client"; import { createMemoryRouter, RouterProvider } from "react-router"; import { preloadImages } from "./assets/images"; -import { Box } from "./components/atoms/box"; import { Dashboard } from "./Dashboard"; import { Providers } from "./providers"; +import { AppContainerProvider } from "./providers/app-container"; import { SettingsContextProvider, useSettings } from "./providers/settings"; import type { SettingsProps, VariantProps } from "./providers/settings/types"; -import { appContainer } from "./style.css"; import { useLoadErrorTranslations } from "./translation"; import { Widget } from "./Widget"; @@ -46,14 +45,11 @@ export const SKApp = (props: SKAppProps) => { return ( - - + ); }; diff --git a/packages/widget/src/components/atoms/virtual-list/index.tsx b/packages/widget/src/components/atoms/virtual-list/index.tsx index 7bbce42e6..a13a6cce2 100644 --- a/packages/widget/src/components/atoms/virtual-list/index.tsx +++ b/packages/widget/src/components/atoms/virtual-list/index.tsx @@ -46,7 +46,7 @@ type VirtualGroupListProps = { estimateSize: VirtualizerOptions["estimateSize"]; } & InfiniteScrollProps; -export const VirtualList = ({ +export const VirtualList = ({ data, itemContent, className, diff --git a/packages/widget/src/hooks/use-media-query.ts b/packages/widget/src/hooks/use-media-query.ts deleted file mode 100644 index ebb20b1c5..000000000 --- a/packages/widget/src/hooks/use-media-query.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useState } from "react"; - -const getMatches = (query: string) => { - if (typeof window === "undefined" || !window.matchMedia) { - return false; - } - - return window.matchMedia(query).matches; -}; - -export const useMediaQuery = (query: string) => { - const [matches, setMatches] = useState(() => getMatches(query)); - - useEffect(() => { - if (typeof window === "undefined" || !window.matchMedia) { - return; - } - - const mediaQueryList = window.matchMedia(query); - const onChange = () => setMatches(mediaQueryList.matches); - - onChange(); - mediaQueryList.addEventListener("change", onChange); - - return () => mediaQueryList.removeEventListener("change", onChange); - }, [query]); - - return matches; -}; diff --git a/packages/widget/src/hooks/use-split-collapsed.ts b/packages/widget/src/hooks/use-split-collapsed.ts new file mode 100644 index 000000000..ae906ed62 --- /dev/null +++ b/packages/widget/src/hooks/use-split-collapsed.ts @@ -0,0 +1,13 @@ +import { useAppContainerWidth } from "../providers/app-container"; +import { isSplitCollapsedWidth } from "../styles/tokens/breakpoints"; + +/** + * JS counterpart of `splitExpandedContainerQuery`. Both must resolve against + * the app container width so the layout stays consistent when the dashboard is + * embedded in a host that is narrower than the viewport. + */ +export const useSplitCollapsed = () => { + const appContainerWidth = useAppContainerWidth(); + + return appContainerWidth !== null && isSplitCollapsedWidth(appContainerWidth); +}; diff --git a/packages/widget/src/pages-dashboard/common/components/split-view/index.tsx b/packages/widget/src/pages-dashboard/common/components/split-view/index.tsx index 5255cc0c0..194859aba 100644 --- a/packages/widget/src/pages-dashboard/common/components/split-view/index.tsx +++ b/packages/widget/src/pages-dashboard/common/components/split-view/index.tsx @@ -2,8 +2,7 @@ import clsx from "clsx"; import { type ReactNode, useState } from "react"; import { Box } from "../../../../components/atoms/box"; import { CaretLeftIcon } from "../../../../components/atoms/icons/caret-left"; -import { useMediaQuery } from "../../../../hooks/use-media-query"; -import { splitCollapsedMediaQuery } from "../../../../styles/tokens/breakpoints"; +import { useSplitCollapsed } from "../../../../hooks/use-split-collapsed"; import { VerticalDivider } from "../divider"; import * as styles from "./styles.css"; @@ -22,7 +21,7 @@ export const SplitView = ({ primaryBarLabel, secondaryBarLabel, }: SplitViewProps) => { - const isCollapsed = useMediaQuery(splitCollapsedMediaQuery); + const isCollapsed = useSplitCollapsed(); const [activeSide, setActiveSide] = useState("primary"); if (!primary || !secondary) { diff --git a/packages/widget/src/pages-dashboard/overview/styles.css.ts b/packages/widget/src/pages-dashboard/overview/styles.css.ts index e9463f858..86cce6ae8 100644 --- a/packages/widget/src/pages-dashboard/overview/styles.css.ts +++ b/packages/widget/src/pages-dashboard/overview/styles.css.ts @@ -1,9 +1,9 @@ import { style } from "@vanilla-extract/css"; -import { splitExpandedMediaQuery } from "../../styles/tokens/breakpoints"; +import { splitExpandedContainerQuery } from "../../styles/tokens/breakpoints"; export const overviewPageContainer = style({ - "@media": { - [splitExpandedMediaQuery]: { + "@container": { + [splitExpandedContainerQuery]: { maxWidth: "380px", }, }, diff --git a/packages/widget/src/pages-dashboard/position-details/styles.css.ts b/packages/widget/src/pages-dashboard/position-details/styles.css.ts index 82529343a..503c43740 100644 --- a/packages/widget/src/pages-dashboard/position-details/styles.css.ts +++ b/packages/widget/src/pages-dashboard/position-details/styles.css.ts @@ -1,6 +1,6 @@ import { style } from "@vanilla-extract/css"; import { atoms } from "../../styles/theme/atoms.css"; -import { splitExpandedMediaQuery } from "../../styles/tokens/breakpoints"; +import { splitExpandedContainerQuery } from "../../styles/tokens/breakpoints"; export const posistionDetailsInfoContainer = style([ atoms({ @@ -9,8 +9,8 @@ export const posistionDetailsInfoContainer = style([ width: "0", }), { - "@media": { - [splitExpandedMediaQuery]: { + "@container": { + [splitExpandedContainerQuery]: { maxWidth: "600px", }, }, @@ -18,8 +18,8 @@ export const posistionDetailsInfoContainer = style([ ]); export const positionDetailsActionsContainer = style({ - "@media": { - [splitExpandedMediaQuery]: { + "@container": { + [splitExpandedContainerQuery]: { maxWidth: "380px", }, }, diff --git a/packages/widget/src/providers/app-container/index.tsx b/packages/widget/src/providers/app-container/index.tsx new file mode 100644 index 000000000..731612d01 --- /dev/null +++ b/packages/widget/src/providers/app-container/index.tsx @@ -0,0 +1,62 @@ +import type { PropsWithChildren } from "react"; +import { createContext, useContext, useLayoutEffect, useState } from "react"; +import { Box } from "../../components/atoms/box"; +import { appContainer } from "../../style.css"; + +type AppContainerVariant = "widget" | "dashboard"; + +/** + * Inline size of the app container, or `null` while it is not measurable yet. + * Layout decisions taken in JS must use this instead of the window size, since + * the app can be embedded in a host that gives it less room than the viewport. + */ +const AppContainerWidthContext = createContext( + undefined +); + +export const AppContainerProvider = ({ + variant, + children, +}: PropsWithChildren<{ variant: AppContainerVariant }>) => { + const [element, setElement] = useState(null); + const [width, setWidth] = useState(null); + + useLayoutEffect(() => { + if (!element || typeof ResizeObserver === "undefined") { + setWidth(null); + return; + } + + setWidth(element.getBoundingClientRect().width); + + const observer = new ResizeObserver(([entry]) => { + if (entry) setWidth(entry.contentRect.width); + }); + + observer.observe(element); + + return () => observer.disconnect(); + }, [element]); + + return ( + + + {children} + + + ); +}; + +export const useAppContainerWidth = () => { + const value = useContext(AppContainerWidthContext); + + if (value === undefined) { + throw new Error("AppContainerProvider not found in the tree"); + } + + return value; +}; diff --git a/packages/widget/src/style.css.ts b/packages/widget/src/style.css.ts index 42be2717b..ec3fb09d6 100644 --- a/packages/widget/src/style.css.ts +++ b/packages/widget/src/style.css.ts @@ -1,10 +1,12 @@ -import { createContainer, style } from "@vanilla-extract/css"; +import { style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; import { atoms } from "./styles/theme/atoms.css"; import { minContainerWidth } from "./styles/tokens/breakpoints"; +import { + appContainerName, + widgetContainerName, +} from "./styles/tokens/containers.css"; -const appContainerName = createContainer(); -const widgetContainerName = createContainer(); const widgetContainerMaxWidth = 400; export const animationContainer = style([ diff --git a/packages/widget/src/styles/tokens/breakpoints.ts b/packages/widget/src/styles/tokens/breakpoints.ts index 83ed52aab..db3840df3 100644 --- a/packages/widget/src/styles/tokens/breakpoints.ts +++ b/packages/widget/src/styles/tokens/breakpoints.ts @@ -1,3 +1,5 @@ +import { appContainerName } from "./containers.css"; + export const breakpoints = { mobile: 0, tablet: 520, @@ -14,19 +16,16 @@ const toPx = (breakpoint: Breakpoint | number) => export const minMediaQuery = (breakpoint: Breakpoint | number) => `screen and (min-width: ${toPx(breakpoint)}px)`; -const maxMediaQuery = (breakpoint: Breakpoint | number) => - `screen and (max-width: ${toPx(breakpoint)}px)`; +export const minContainerWidth = ( + containerName: string, + breakpoint: Breakpoint | number +) => `${containerName} (min-width: ${toPx(breakpoint)}px)`; -export const splitCollapsedMediaQuery = maxMediaQuery( - SPLIT_COLLAPSE_BREAKPOINT -); -export const splitExpandedMediaQuery = minMediaQuery( +export const splitExpandedContainerQuery = minContainerWidth( + appContainerName, SPLIT_COLLAPSE_BREAKPOINT + 1 ); -export const minContainerWidth = ( - containerName: string, - breakpoint: Breakpoint | number -) => - `${containerName} (min-width: ${typeof breakpoint === "number" ? breakpoint : breakpoints[breakpoint]}px)`; +export const isSplitCollapsedWidth = (appContainerWidth: number) => + appContainerWidth <= SPLIT_COLLAPSE_BREAKPOINT; diff --git a/packages/widget/src/styles/tokens/containers.css.ts b/packages/widget/src/styles/tokens/containers.css.ts new file mode 100644 index 000000000..085b0bd72 --- /dev/null +++ b/packages/widget/src/styles/tokens/containers.css.ts @@ -0,0 +1,5 @@ +import { createContainer } from "@vanilla-extract/css"; + +export const appContainerName = createContainer(); + +export const widgetContainerName = createContainer();