improve mobile mode

This commit is contained in:
SrGooglo 2023-06-09 14:26:01 +00:00
parent 163cf09eab
commit c7a554e455
34 changed files with 545 additions and 368 deletions

View File

@ -88,7 +88,7 @@ import { DOMWindow } from "components/RenderWindow"
import { Icons } from "components/Icons"
import { ThemeProvider } from "cores/style"
import { ThemeProvider } from "cores/style/style.core.jsx"
import Layout from "./layout"
import * as Router from "./router"

View File

@ -74,7 +74,7 @@ export default class Drawer extends Component {
ALLOW_DRAWER_TRANSFORM = true
componentDidMount() {
this.DESKTOP_MODE = !window.isMobile
this.DESKTOP_MODE = !app.isMobile
}
componentDidUpdate(prevProps, nextState) {

View File

@ -6,31 +6,39 @@ import { Motion, spring } from "react-motion"
import { Icons, createIconRender } from "components/Icons"
import { WithPlayerContext, Context } from "contexts/WithPlayerContext"
import PlayerView from "pages/@mobile-views/player"
import "./index.less"
const items = [
{
id: "creator",
dispatchEvent: "app.openCreator",
icon: "PlusCircle",
classnames: [["primary"]]
},
{
id: "feed",
location: "/home/feed",
icon: "Home",
},
{
id: "explore",
location: "/home/explore",
icon: "Search",
},
{
id: "livestreams",
location: "/home/livestreams",
icon: "Tv",
const PlayerButton = (props) => {
const openPlayerView = () => {
app.DrawerController.open("player", PlayerView)
}
]
React.useEffect(() => {
openPlayerView()
}, [])
return <div
className={classnames(
"player_btn",
{
"bounce": props.playback === "playing"
}
)}
style={{
"--average-color": props.colorAnalysis?.rgba,
"--color": props.colorAnalysis?.isDark ? "var(--text-color-white)" : "var(--text-color-black)",
}}
onClick={openPlayerView}
>
{
props.playback === "playing" ? <Icons.MdMusicNote /> : <Icons.MdPause />
}
</div>
}
const AccountButton = (props) => {
const user = app.userData
@ -51,26 +59,10 @@ const AccountButton = (props) => {
key: "settings",
text: <><Icons.Settings /> <span>Settings</span></>,
onClick: () => {
app.openSettings()
app.navigation.goToSettings()
ActionSheetRef.current.close()
}
},
{
key: "savedPosts",
text: <><Icons.Bookmark /> <span>Saved Posts</span></>,
onClick: () => {
app.setLocation("/home/savedPosts")
ActionSheetRef.current.close()
}
},
{
key: "about",
text: <><Icons.Info /> <span>About</span></>,
onClick: () => {
app.setLocation("/about")
ActionSheetRef.current.close()
}
}
]
})
}
@ -91,7 +83,17 @@ const AccountButton = (props) => {
</div>
}
export default class BottomBar extends React.Component {
export default (props) => {
return <WithPlayerContext>
<BottomBar
{...props}
/>
</WithPlayerContext>
}
export class BottomBar extends React.Component {
static contextType = Context
state = {
allowed: true,
show: true,
@ -133,7 +135,7 @@ export default class BottomBar extends React.Component {
}
toggleVisibility = (to) => {
if (!window.isMobile) {
if (!app.isMobile) {
to = false
} else {
to = to ?? !this.state.visible
@ -160,21 +162,6 @@ export default class BottomBar extends React.Component {
}
}
renderItems = () => {
return items.map((item) => {
return <div
key={item.id}
id={item.id}
className={classnames("item", ...item.classnames ?? [])}
onClick={() => this.handleItemClick(item)}
>
<div className="icon">
{createIconRender(item.icon)}
</div>
</div>
})
}
render() {
if (this.state.render) {
return <div className="bottomBar">
@ -195,7 +182,51 @@ export default class BottomBar extends React.Component {
}}
>
<div className="items">
{this.renderItems()}
<div
key="creator"
id="creator"
className={classnames("item", "primary")}
onClick={() => app.setLocation("/")}
>
<div className="icon">
{createIconRender("PlusCircle")}
</div>
</div>
{
this.context.currentManifest && <div
className="item"
>
<PlayerButton
manifest={this.context.currentManifest}
playback={this.context.playbackStatus}
colorAnalysis={this.context.coverColorAnalysis}
/>
</div>
}
<div
key="navigator"
id="navigator"
className="item"
onClick={() => app.setLocation("/")}
>
<div className="icon">
{createIconRender("Home")}
</div>
</div>
<div
key="searcher"
id="searcher"
className="item"
onClick={app.controls.openSearcher}
>
<div className="icon">
{createIconRender("Search")}
</div>
</div>
<AccountButton />
</div>
</div>}

View File

@ -1,4 +1,26 @@
@import "theme/vars.less";
@import "theme/animations.less";
.player_btn {
color: var(--color);
background-color: var(--average-color);
padding: 10px 20px;
border-radius: 8px;
&.bounce {
svg {
animation: bounce 1s infinite;
}
}
svg {
color: var(--color);
margin: 0;
}
}
.bottomBar {
display: flex;
@ -24,6 +46,8 @@
background-color: var(--background-color-accent);
border-radius: 12px 12px 0 0;
box-shadow: @card-shadow;
.items {
display: inline-flex;
align-items: center;

View File

@ -227,7 +227,7 @@ export default class Login extends React.Component {
>
<span><Icons.Lock /> Password</span>
<antd.Input.Password
placeholder="********"
//placeholder="********"
onChange={(e) => this.onUpdateInput("password", e.target.value)}
onPressEnter={this.nextStep}
/>

View File

@ -0,0 +1,59 @@
import React from "react"
import classnames from "classnames"
import * as antd from "antd"
import { createIconRender } from "components/Icons"
import "./index.less"
const NavMenu = (props) => {
const handleOnClickItem = (event) => {
return props.onClickItem(event.key)
}
return <div className="navmenu_wrapper">
{
props.header && <div className="card header" id="navMenu">
{props.header}
</div>
}
<div className="card content" id="navMenu">
<antd.Menu
mode="inline"
selectedKeys={[props.activeKey]}
onClick={handleOnClickItem}
items={props.items}
/>
</div>
</div>
}
const NavMenuMobile = (props) => {
return <div className="__mobile__navmenu_wrapper">
<div className="card">
{
props.items.map((item) => {
return <div
key={item.key}
className={classnames(
"item",
item.key === props.activeKey && "active",
)}
onClick={() => props.onClickItem(item.key)}
>
<div className="icon">
{item.icon}
</div>
<div className="label">
{item.label}
</div>
</div>
})
}
</div>
</div>
}
export default app.isMobile ? NavMenuMobile : NavMenu

View File

@ -0,0 +1,21 @@
.navmenu_wrapper {
position: relative;
}
.__mobile__navmenu_wrapper {
box-sizing: border-box;
position: sticky;
top: 0;
left: 0;
width: 100%;
.card {
display: flex;
flex-direction: row;
gap: 10px;
}
}

View File

@ -4,6 +4,8 @@ import * as antd from "antd"
import { createIconRender } from "components/Icons"
import NavMenu from "./components/NavMenu"
import "./index.less"
export const Panel = (props) => {
@ -19,29 +21,6 @@ export const Panel = (props) => {
</div>
}
const NavMenu = (props) => {
const handleOnClickItem = (event) => {
return props.onClickItem(event.key)
}
return <div className="navmenu_wrapper">
{
props.header && <div className="card header" id="navMenu">
{props.header}
</div>
}
<div className="card content" id="navMenu">
<antd.Menu
mode="inline"
selectedKeys={[props.activeKey]}
onClick={handleOnClickItem}
items={props.items}
/>
</div>
</div>
}
export class PagePanelWithNavMenu extends React.Component {
state = {
// if defaultTab is not set, try to get it from query, if not, use the first tab

View File

@ -1,3 +1,5 @@
@import "theme/animations.less";
.background_media_player {
position: relative;
@ -234,23 +236,4 @@
}
}
}
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-5px);
}
60% {
transform: translateY(-3px);
}
}

View File

@ -79,23 +79,25 @@ export default ({
onClick={() => onClickActionsButton("next")}
disabled={syncModeLocked}
/>
<antd.Popover
content={React.createElement(
AudioVolume,
{ onChange: onVolumeUpdate, defaultValue: audioVolume }
)}
trigger="hover"
>
<div
className="muteButton"
onClick={onMuteUpdate}
{
!app.isMobile && <antd.Popover
content={React.createElement(
AudioVolume,
{ onChange: onVolumeUpdate, defaultValue: audioVolume }
)}
trigger="hover"
>
{
audioMuted
? <Icons.VolumeX />
: <Icons.Volume2 />
}
</div>
</antd.Popover>
<div
className="muteButton"
onClick={onMuteUpdate}
>
{
audioMuted
? <Icons.VolumeX />
: <Icons.Volume2 />
}
</div>
</antd.Popover>
}
</div>
}

View File

@ -90,8 +90,9 @@ export class AudioPlayer extends React.Component {
className={classnames(
"embbededMediaPlayerWrapper",
{
["hovering"]: this.state.showControls,
["hovering"]: this.props.frame !== false && this.state.showControls,
["minimized"]: this.context.minimized,
["no-frame"]: this.props.frame === false,
}
)}
onMouseEnter={this.onMouse}

View File

@ -18,6 +18,31 @@
transition: all 150ms ease-in-out;
&.no-frame {
width: 100%;
height: 100%;
flex-direction: column;
.player {
background-color: transparent;
border-radius: 0;
box-shadow: none;
}
.top_controls {
position: relative;
opacity: 1;
height: @top_controls_height;
width: 100%;
background-color: transparent;
box-shadow: none;
}
}
&.minimized {
pointer-events: none;

View File

@ -98,10 +98,14 @@
font-size: 0.9rem;
color: var(--text-color);
.playlistTimelineEntry_statistic {
display: flex;
flex-direction: row;
color: var(--text-color);
justify-content: center;
align-items: center;
}

View File

@ -5,7 +5,7 @@
width: 35vw;
min-width: 300px;
max-width: 800px;
//max-width: 600px;
//min-height: 165px;
//height: 100%;

View File

@ -368,95 +368,43 @@ export class PostsListsComponent extends React.Component {
return <div className="post-list_wrapper">
<LoadMore
ref={this.listRef}
className="post-list"
loadingComponent={LoadingComponent}
noResultComponent={NoResultComponent}
hasMore={this.state.hasMore}
fetching={this.state.loading}
onBottom={this.onLoadMore}
>
{
!this.state.realtimeUpdates && <div className="resume_btn_wrapper">
<antd.Button
type="primary"
shape="round"
onClick={this.onResumeRealtimeUpdates}
loading={this.state.resumingLoading}
icon={<Icons.SyncOutlined />}
>
Resume
</antd.Button>
</div>
}
{
this.state.list.map((data) => {
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
key: data._id,
data: data,
events: {
onClickLike: this.onLikePost,
onClickSave: this.onSavePost,
onClickDelete: this.onDeletePost,
onClickEdit: this.onEditPost,
}
})
})
}
</LoadMore>
</div>
return <AutoSizer
disableWidth
style={{
width: "100%",
}}
>
{({ height }) => {
console.log("[PostList] AutoSizer height update => ", height)
return <LoadMore
ref={this.listRef}
contentProps={{
style: { height }
}}
className="postList"
loadingComponent={LoadingComponent}
noResultComponent={NoResultComponent}
hasMore={this.state.hasMore}
fetching={this.state.loading}
onBottom={this.onLoadMore}
>
{
!this.state.realtimeUpdates && <div className="resume_btn_wrapper">
<antd.Button
type="primary"
shape="round"
onClick={this.onResumeRealtimeUpdates}
loading={this.state.resumingLoading}
icon={<Icons.SyncOutlined />}
>
Resume
</antd.Button>
</div>
}
{
this.state.list.map((data) => {
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
key: data._id,
data: data,
events: {
onClickLike: this.onLikePost,
onClickSave: this.onSavePost,
onClickDelete: this.onDeletePost,
onClickEdit: this.onEditPost,
}
})
ref={this.listRef}
className="post-list"
loadingComponent={LoadingComponent}
noResultComponent={NoResultComponent}
hasMore={this.state.hasMore}
fetching={this.state.loading}
onBottom={this.onLoadMore}
>
{
!this.state.realtimeUpdates && !app.isMobile && <div className="resume_btn_wrapper">
<antd.Button
type="primary"
shape="round"
onClick={this.onResumeRealtimeUpdates}
loading={this.state.resumingLoading}
icon={<Icons.SyncOutlined />}
>
Resume
</antd.Button>
</div>
}
{
this.state.list.map((data) => {
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
key: data._id,
data: data,
events: {
onClickLike: this.onLikePost,
onClickSave: this.onSavePost,
onClickDelete: this.onDeletePost,
onClickEdit: this.onEditPost,
}
})
}
</LoadMore>
}}
</AutoSizer>
})
}
</LoadMore>
</div>
}
}

View File

@ -33,7 +33,6 @@
margin: auto;
z-index: 150;
}
.resume_btn_wrapper {

View File

@ -51,12 +51,8 @@ export default class Layout extends React.PureComponent {
}
componentDidMount() {
if (window.app.cores.settings.get("forceMobileMode") || window.app.capacitor.isAppCapacitor() || Math.min(window.screen.width, window.screen.height) < 768 || navigator.userAgent.indexOf("Mobi") > -1) {
window.isMobile = true
if (window.app.cores.settings.get("forceMobileMode") || app.isMobile) {
app.layout.set("mobile")
} else {
window.isMobile = false
}
// register events
@ -147,7 +143,7 @@ export default class Layout extends React.PureComponent {
return JSON.stringify(this.state.renderError)
}
const Layout = Layouts[layoutType]
const Layout = Layouts[app.isMobile ? "mobile" : layoutType]
if (!Layout) {
return app.eventBus.emit("runtime.crash", new Error(`Layout type [${layoutType}] not found`))

View File

@ -9,11 +9,9 @@ export default (props) => {
<Modal />
<antd.Layout.Content className={classnames("content_layout", ...props.layoutPageModesClassnames ?? [])}>
<div className={classnames("frameDecorator", "top")} />
<div id="transitionLayer" className={classnames("content_wrapper", "fade-transverse-active")}>
{React.cloneElement(props.children, props)}
</div>
<div className={classnames("frameDecorator", "bottom")} />
</antd.Layout.Content>
<BottomBar />

View File

@ -5,7 +5,7 @@ import classnames from "classnames"
import { Drawer, Sidedrawer } from "components/Layout"
export default (props) => {
return <antd.Layout className={classnames("app_layout", { ["mobile"]: window.isMobile })} style={{ height: "100%" }}>
return <antd.Layout className={classnames("app_layout", { ["mobile"]: app.isMobile })} style={{ height: "100%" }}>
<Drawer />
<Sidedrawer />
<div id="transitionLayer" className="fade-transverse-active">

View File

@ -0,0 +1,12 @@
import React from "react"
import MediaPlayer from "components/Player/MediaPlayer"
import "./index.less"
export default () => {
return <div className="__mobile-player-view">
<MediaPlayer
frame={false}
/>
</div>
}

View File

@ -0,0 +1,8 @@
.__mobile-player-view {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}

View File

@ -258,7 +258,7 @@ export default class Account extends React.Component {
<div className="tabMenuWrapper">
<antd.Menu
className="tabMenu"
mode={window.isMobile ? "horizontal" : "vertical"}
mode={app.isMobile ? "horizontal" : "vertical"}
selectedKeys={[this.state.tabActiveKey]}
onClick={(e) => this.handlePageTransition(e.key)}
>

View File

@ -0,0 +1,35 @@
import React from "react"
import * as antd from "antd"
import { Translation } from "react-i18next"
import { PagePanelWithNavMenu } from "components/PagePanels"
import { Icons } from "components/Icons"
import Tabs from "./home/tabs"
export default class Home extends React.Component {
render() {
const navMenuHeader = <>
<h1>
<Icons.Home />
<Translation>{(t) => t("Timeline")}</Translation>
</h1>
<antd.Button
type="primary"
onClick={app.controls.openPostCreator}
>
<Icons.PlusCircle />
<Translation>{(t) => t("Create")}</Translation>
</antd.Button>
</>
return <PagePanelWithNavMenu
tabs={Tabs}
navMenuHeader={navMenuHeader}
primaryPanelClassName="full"
useSetQueryType
transition
/>
}
}

View File

@ -75,7 +75,7 @@ export default (props) => {
<div className="content">
<div className="content_header">
<img src={window.isMobile ? config.logo.alt : config.logo.full} className="logo" />
<img src={app.isMobile ? config.logo.alt : config.logo.full} className="logo" />
</div>
{

View File

@ -1,12 +1,15 @@
import React from "react"
import "./index.less"
import "./index.mobile.less"
export default (props) => {
const [wallpaperData, setWallpaperData] = React.useState(null)
const setRandomWallpaper = async () => {
const featuredWallpapers = await app.cores.api.request("main", "get", "featuredWallpapers").catch((err) => {
const { data: featuredWallpapers } = await app.cores.api.customRequest({
method: "GET",
url: "/featured_wallpapers"
}).catch((err) => {
console.error(err)
return []
})
@ -19,12 +22,12 @@ export default (props) => {
React.useEffect(() => {
if (app.userData) {
return app.goMain()
return app.navigation.goMain()
}
setRandomWallpaper()
app.eventBus.emit("app.createLogin", {
app.controls.openLoginForm({
defaultLocked: true,
})
}, [])
@ -36,9 +39,9 @@ export default (props) => {
}}
className="wallpaper"
>
<p>
{/* <p>
{wallpaperData?.author ? wallpaperData.author : null}
</p>
</p> */}
</div>
</div>
}

View File

@ -0,0 +1,22 @@
.loginPage {
position: relative;
width: 100%;
height: 100vh;
height: 100dvh;
.wallpaper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
height: 100dvh;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
}

View File

@ -601,10 +601,16 @@ export default class SyncLyrics extends React.Component {
colorAnalysis,
})
app.SidebarController.toggleVisibility(false)
if (app.SidebarController) {
app.SidebarController.toggleVisibility(false)
}
if (app.layout.floatingStack) {
app.layout.floatingStack.toogleGlobalVisibility(false)
}
app.cores.style.compactMode(true)
app.cores.style.applyVariant("dark")
app.layout.floatingStack.toogleGlobalVisibility(false)
// request full screen to browser
if (document.fullscreenEnabled) {
@ -640,10 +646,16 @@ export default class SyncLyrics extends React.Component {
delete window._hacks
app.SidebarController.toggleVisibility(true)
if (app.SidebarController) {
app.SidebarController.toggleVisibility(true)
}
if (app.layout.floatingStack) {
app.layout.floatingStack.toogleGlobalVisibility(true)
}
app.cores.style.compactMode(false)
app.cores.style.applyInitialVariant()
app.layout.floatingStack.toogleGlobalVisibility(true)
// exit full screen
if (document.fullscreenEnabled) {

View File

@ -279,7 +279,9 @@ export default class PlaylistCreatorSteps extends React.Component {
okType: "danger",
cancelText: "Cancel",
onOk: async () => {
const result = await PlaylistModel.deletePlaylist(this.props.playlist_id)
const result = await PlaylistModel.deletePlaylist(this.props.playlist_id, {
remove_with_tracks: true
})
if (result) {
app.message.success("Playlist deleted")

View File

@ -105,7 +105,7 @@ export default (props) => {
return <div
className={
classnames("play", playlist.type)
classnames("playlist_view", playlist.type)
}
>
<div className="play_info_wrapper">

View File

@ -1,4 +1,4 @@
.play {
.playlist_view {
display: grid;
grid-template-columns: 30vw 1fr;
@ -54,15 +54,12 @@
background-color: black;
border-radius: 12px;
overflow: hidden;
img {
width: 100%;
height: 100%;
border-radius: 12px;
object-fit: cover;
}
}

View File

@ -591,7 +591,7 @@ export default () => {
className={classnames(
"settings_wrapper",
{
["mobile"]: window.isMobile,
["mobile"]: app.isMobile,
}
)}
>

View File

@ -46,7 +46,7 @@ const mobileRoutes = Object.keys(pathsMobile).map((route) => {
return {
path,
element: pathsMobile[path]
element: pathsMobile[route]
}
})
@ -192,13 +192,11 @@ export const PageRender = React.memo((props) => {
return <Routes>
{
routes.map((route, index) => {
let Element = route.element
if (app.isMobile) {
const mobileRoute = mobileRoutes.find((r) => r.path === route.path)
if (window.isMobile) {
const mobileElement = mobileRoutes.find((r) => r.path === route.path)
if (mobileElement) {
Element = mobileElement
if (mobileRoute) {
route = mobileRoute
}
}

View File

@ -93,4 +93,23 @@
to {
opacity: 0;
}
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-5px);
}
60% {
transform: translateY(-3px);
}
}

View File

@ -10,8 +10,10 @@
.content_wrapper {
height: 100%;
width: 100%;
overflow-y: scroll;
padding-top: 20px;
//padding-top: 20px;
}
.content_layout {
@ -22,142 +24,139 @@
align-items: center;
justify-content: center;
width: 100vw;
padding: 0 10px;
width: 100%;
padding: 7px;
box-sizing: border-box;
}
.frameDecorator {
position: absolute;
left: 0;
z-index: 8000;
width: 100vw;
height: @app_frameDecorator_height;
background-color: rgba(var(--layoutBackgroundColor), 0.8);
filter: blur(4px);
backdrop-filter: blur(10px);
&.top {
top: 0;
transform: translate(0, -6px);
}
&.bottom {
bottom: 0;
transform: translate(0, 6px);
}
}
// FIXMETS FOR MOBILE LAYOUT
.loginPage {
flex-direction: column;
.wallpaper {
width: 100vw;
}
}
.dashboard {
display: flex;
flex-direction: column;
}
.postPage {
.playlist_view {
display: flex;
flex-direction: column;
height: 90vh;
.play_info_wrapper {
position: relative;
.postCard {
&.fullmode {
.wrapper {
height: 76vh;
.play_info {
width: 100%;
.post_content {
.post_attachments {
height: 60vh;
overflow: hidden;
.play_info_cover {
width: 100%;
height: 100%;
}
}
}
.carousel-root {
.carousel {
min-height: 200px;
.list {
padding: 30px 10px;
}
}
>div {
display: flex;
align-items: center;
height: 100%;
}
}
}
.pagePanels {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: visible;
width: 100%;
height: 100%;
.panel {
width: 100%;
//height: 100%;
&.left {
z-index: 500;
overflow: visible;
overflow-x: visible;
}
&.right {
z-index: 400;
overflow-x: visible;
}
}
.card {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
padding: 5px;
justify-content: space-between;
box-shadow: @card-shadow;
border-radius: 12px;
isolation: unset;
overflow: visible;
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px 10px;
border-radius: 8px;
.icon {
svg {
margin: 0 !important;
font-size: 1rem;
}
}
.label {
font-size: 0.6rem;
}
&.active {
.icon {
svg {
color: var(--colorPrimary);
}
}
.label {
color: var(--colorPrimary);
}
background-color: var(--background-color-primary);
}
}
}
}
.post-list_wrapper {
.post-list {
padding-top: 10px;
border-radius: 0;
.playlistTimelineEntry {
width: 100%;
.playlistTimelineEntry_content {
.playlistTimelineEntry_thumbnail {
img {
width: 20vw;
height: 20vw;
}
}
}
}
}
}
.accountProfile {
padding: 0;
.contents {
display: flex;
flex-direction: column;
padding: 0;
margin-top: -4.0vh;
padding: 0;
.tabContent {
padding: 0;
padding-top: 26px;
}
.tabMenuWrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
margin: 0;
.ant-menu {
padding: 0;
margin: 0;
}
}
}
}
.liveStream {
flex-direction: column;
.plyr {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 60vh;
border-radius: 6px;
}
.panel {
height: 40vh;
}
}
.postCard {
.post_actionsWrapper {
.actions {
.postCard {
width: 100%;
}
}