diff --git a/packages/widget/src/App.tsx b/packages/widget/src/App.tsx index 8bc4be7c..39c123d1 100644 --- a/packages/widget/src/App.tsx +++ b/packages/widget/src/App.tsx @@ -47,6 +47,7 @@ export const SKApp = (props: SKAppProps) => { return ( { + 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 00000000..5255cc0c --- /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 00000000..26b2aa27 --- /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 bf83fa85..386c1d9b 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 deleted file mode 100644 index d0e4d84e..00000000 --- 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}; -}; diff --git a/packages/widget/src/pages-dashboard/overview/index.tsx b/packages/widget/src/pages-dashboard/overview/index.tsx index 85a4edac..b91fceba 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 7b03c1d3..e9463f85 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 12d62248..df734cf1 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 b4d05886..82529343 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/styles/theme/global.css.ts b/packages/widget/src/styles/theme/global.css.ts index 23b2b73a..1416e937 100644 --- a/packages/widget/src/styles/theme/global.css.ts +++ b/packages/widget/src/styles/theme/global.css.ts @@ -1,6 +1,6 @@ import { globalStyle, layer } from "@vanilla-extract/css"; import { vars } from "./contract.css"; -import { rootSelector } from "./ids"; +import { dashboardLayoutSelector, rootSelector } from "./ids"; const reset = layer("reset"); @@ -12,6 +12,13 @@ globalStyle(rootSelector, { }, }); +globalStyle(`${dashboardLayoutSelector} ${rootSelector}`, { + marginLeft: "auto", + marginRight: "auto", + maxWidth: "1000px", + width: "100%", +}); + // Resets globalStyle(`${rootSelector} *`, { "@layer": { diff --git a/packages/widget/src/styles/theme/ids.ts b/packages/widget/src/styles/theme/ids.ts index 51b7c159..c5eee43b 100644 --- a/packages/widget/src/styles/theme/ids.ts +++ b/packages/widget/src/styles/theme/ids.ts @@ -1,2 +1,5 @@ export const id = "stakekit"; export const rootSelector = `[data-rk="${id}"]`; + +const layoutAttribute = "data-sk-layout"; +export const dashboardLayoutSelector = `[${layoutAttribute}="dashboard"]`; diff --git a/packages/widget/src/styles/tokens/breakpoints.ts b/packages/widget/src/styles/tokens/breakpoints.ts index 124690d4..83ed52aa 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 cac12c71..a0cf90cd 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 a508d49a..5be0f9ee 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",