navigation redesign

This commit is contained in:
Sean Yesmunt 2020-01-02 11:30:27 -05:00
parent e7c1085faa
commit a8711c027f
33 changed files with 499 additions and 219 deletions

View file

@ -936,5 +936,12 @@
"Text copied": "Text copied", "Text copied": "Text copied",
"Rewards Disabled": "Rewards Disabled", "Rewards Disabled": "Rewards Disabled",
"Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!.": "Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!.", "Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!.": "Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!.",
"Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.": "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%." "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.": "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.",
} "Your Tags": "Your Tags",
"All Content": "All Content",
"Claim Your 5 LBC Invite Reward": "Claim Your 5 LBC Invite Reward",
"Accepted": "Accepted",
"Claimable": "Claimable",
"Invite A Friend": "Invite A Friend",
"Navigation": "Navigation"
}

View file

@ -46,7 +46,7 @@ type LogPublishParams = {
let analyticsEnabled: boolean = true; let analyticsEnabled: boolean = true;
const analytics: Analytics = { const analytics: Analytics = {
error: message => { error: message => {
if (analyticsEnabled) { if (analyticsEnabled && isProduction) {
Lbryio.call('event', 'desktop_error', { error_message: message }); Lbryio.call('event', 'desktop_error', { error_message: message });
} }
}, },

View file

@ -147,6 +147,13 @@ function App(props: Props) {
} }
}, [previousRewardApproved, isRewardApproved]); }, [previousRewardApproved, isRewardApproved]);
// @if TARGET='app'
useEffect(() => {
console.log('update prefs');
updatePreferences();
}, []);
// @endif
// Keep this at the end to ensure initial setup effects are run first // Keep this at the end to ensure initial setup effects are run first
useEffect(() => { useEffect(() => {
// Wait for balance to be populated on desktop so we know when we can begin syncing // Wait for balance to be populated on desktop so we know when we can begin syncing

View file

@ -7,14 +7,12 @@ import {
doToggleTagFollow, doToggleTagFollow,
selectBlockedChannels, selectBlockedChannels,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import ClaimListDiscover from './view'; import ClaimListDiscover from './view';
const select = state => ({ const select = state => ({
claimSearchByQuery: selectClaimSearchByQuery(state), claimSearchByQuery: selectClaimSearchByQuery(state),
loading: selectFetchingClaimSearch(state), loading: selectFetchingClaimSearch(state),
subscribedChannels: selectSubscriptions(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state), showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
hiddenUris: selectBlockedChannels(state), hiddenUris: selectBlockedChannels(state),
}); });

View file

@ -1,6 +1,6 @@
// @flow // @flow
import type { Node } from 'react'; import type { Node } from 'react';
import * as PAGES from 'constants/pages'; import classnames from 'classnames';
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux'; import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
@ -8,7 +8,6 @@ import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import moment from 'moment'; import moment from 'moment';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import Tag from 'component/tag';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import { toCapitalCase } from 'util/string'; import { toCapitalCase } from 'util/string';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
@ -19,15 +18,12 @@ const TIME_WEEK = 'week';
const TIME_MONTH = 'month'; const TIME_MONTH = 'month';
const TIME_YEAR = 'year'; const TIME_YEAR = 'year';
const TIME_ALL = 'all'; const TIME_ALL = 'all';
const SEARCH_SORT_YOU = 'you';
const SEARCH_SORT_ALL = 'everyone';
const SEARCH_SORT_CHANNELS = 'channels';
const TYPE_TRENDING = 'trending'; export const TYPE_TRENDING = 'trending';
const TYPE_TOP = 'top'; export const TYPE_TOP = 'top';
const TYPE_NEW = 'new'; export const TYPE_NEW = 'new';
const SEARCH_FILTER_TYPES = [SEARCH_SORT_YOU, SEARCH_SORT_CHANNELS, SEARCH_SORT_ALL];
const SEARCH_TYPES = [TYPE_TRENDING, TYPE_TOP, TYPE_NEW]; const SEARCH_TYPES = [TYPE_TRENDING, TYPE_NEW, TYPE_TOP];
const SEARCH_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL]; const SEARCH_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL];
type Props = { type Props = {
@ -40,7 +36,6 @@ type Props = {
doToggleTagFollow: string => void, doToggleTagFollow: string => void,
meta?: Node, meta?: Node,
showNsfw: boolean, showNsfw: boolean,
hideCustomization: boolean,
history: { action: string, push: string => void, replace: string => void }, history: { action: string, push: string => void, replace: string => void },
location: { search: string, pathname: string }, location: { search: string, pathname: string },
claimSearchByQuery: { claimSearchByQuery: {
@ -48,6 +43,8 @@ type Props = {
}, },
hiddenUris: Array<string>, hiddenUris: Array<string>,
hiddenNsfwMessage?: Node, hiddenNsfwMessage?: Node,
channelIds?: Array<string>,
defaultTypeSort?: string,
}; };
function ClaimListDiscover(props: Props) { function ClaimListDiscover(props: Props) {
@ -58,21 +55,20 @@ function ClaimListDiscover(props: Props) {
loading, loading,
personalView, personalView,
meta, meta,
subscribedChannels, channelIds,
showNsfw, showNsfw,
history, history,
location, location,
hiddenUris, hiddenUris,
hideCustomization,
hiddenNsfwMessage, hiddenNsfwMessage,
defaultTypeSort,
} = props; } = props;
const didNavigateForward = history.action === 'PUSH'; const didNavigateForward = history.action === 'PUSH';
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const { search } = location; const { search } = location;
const [forceRefresh, setForceRefresh] = useState(); const [forceRefresh, setForceRefresh] = useState();
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const personalSort = urlParams.get('sort') || (hideCustomization ? SEARCH_SORT_ALL : SEARCH_SORT_YOU); const typeSort = urlParams.get('type') || defaultTypeSort || TYPE_TRENDING;
const typeSort = urlParams.get('type') || TYPE_TRENDING;
const timeSort = urlParams.get('time') || TIME_WEEK; const timeSort = urlParams.get('time') || TIME_WEEK;
const tagsInUrl = urlParams.get('t') || ''; const tagsInUrl = urlParams.get('t') || '';
const options: { const options: {
@ -91,8 +87,8 @@ function ClaimListDiscover(props: Props) {
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination // no_totals makes it so the sdk doesn't have to calculate total number pages for pagination
// it's faster, but we will need to remove it if we start using total_pages // it's faster, but we will need to remove it if we start using total_pages
no_totals: true, no_totals: true,
any_tags: (personalView && !hideCustomization && personalSort === SEARCH_SORT_YOU) || !personalView ? tags : [], any_tags: tags || [],
channel_ids: personalSort === SEARCH_SORT_CHANNELS ? subscribedChannels.map(sub => sub.uri.split('#')[1]) : [], channel_ids: channelIds || [],
not_channel_ids: hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [], not_channel_ids: hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
not_tags: !showNsfw ? MATURE_TAGS : [], not_tags: !showNsfw ? MATURE_TAGS : [],
order_by: order_by:
@ -110,41 +106,17 @@ function ClaimListDiscover(props: Props) {
.unix() .unix()
)}`; )}`;
} }
const hasContent =
(personalSort === SEARCH_SORT_CHANNELS && subscribedChannels.length) || const hasMatureTags = tags && tags.some(t => MATURE_TAGS.includes(t));
(personalSort === SEARCH_SORT_YOU && !!tags.length) ||
personalSort === SEARCH_SORT_ALL;
const hasMatureTags = tags.some(t => MATURE_TAGS.includes(t));
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options); const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
const uris = (hasContent && claimSearchByQuery[claimSearchCacheQuery]) || []; const uris = claimSearchByQuery[claimSearchCacheQuery] || [];
const shouldPerformSearch = const shouldPerformSearch =
hasContent && uris.length === 0 ||
(uris.length === 0 || didNavigateForward ||
didNavigateForward || (!loading && uris.length < PAGE_SIZE * page && uris.length % PAGE_SIZE === 0);
(!loading && uris.length < PAGE_SIZE * page && uris.length % PAGE_SIZE === 0));
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time // Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
const optionsStringForEffect = JSON.stringify(options); const optionsStringForEffect = JSON.stringify(options);
const noChannels = (
<div>
<p>
<I18nMessage>You're not following any channels.</I18nMessage>
</p>
<p>
<I18nMessage
tokens={{
trending: (
<Button button="link" label={__('trending for everyone')} navigate={'/?type=trending&sort=everyone'} />
),
discover: <Button button="link" label={__('discover some channels!')} navigate={'/$/following'} />,
}}
>
Look what's %trending% or %discover%
</I18nMessage>
</p>
</div>
);
const noResults = ( const noResults = (
<div> <div>
<p> <p>
@ -174,22 +146,6 @@ function ClaimListDiscover(props: Props) {
</div> </div>
); );
const noTags = (
<p>
<I18nMessage
tokens={{
customize: <Button button="link" navigate={`/$/${PAGES.FOLLOWING}`} label={__('customize')} />,
}}
>
You're not following any tags. Add tags above or smash that %customize% button!
</I18nMessage>
</p>
);
const noFollowing =
(personalSort === SEARCH_SORT_YOU && noTags) || (personalSort === SEARCH_SORT_CHANNELS && noChannels);
const emptyState = !loading && !hasContent ? noFollowing : noResults;
function getSearch() { function getSearch() {
let search = `?`; let search = `?`;
if (!personalView) { if (!personalView) {
@ -200,7 +156,7 @@ function ClaimListDiscover(props: Props) {
} }
function handleTypeSort(newTypeSort) { function handleTypeSort(newTypeSort) {
let url = `${getSearch()}type=${newTypeSort}&sort=${personalSort}`; let url = `${getSearch()}type=${newTypeSort}`;
if (newTypeSort === TYPE_TOP) { if (newTypeSort === TYPE_TOP) {
url += `&time=${timeSort}`; url += `&time=${timeSort}`;
} }
@ -209,14 +165,9 @@ function ClaimListDiscover(props: Props) {
history.push(url); history.push(url);
} }
function handlePersonalSort(newPersonalSort) {
setPage(1);
history.push(`${getSearch()}type=${typeSort}&sort=${newPersonalSort}`);
}
function handleTimeSort(newTimeSort) { function handleTimeSort(newTimeSort) {
setPage(1); setPage(1);
history.push(`${getSearch()}type=${typeSort}&sort=${personalSort}&time=${newTimeSort}`); history.push(`${getSearch()}type=${typeSort}&time=${newTimeSort}`);
} }
function handleScrollBottom() { function handleScrollBottom() {
@ -234,47 +185,19 @@ function ClaimListDiscover(props: Props) {
const header = ( const header = (
<Fragment> <Fragment>
<FormField {SEARCH_TYPES.map(type => (
className="claim-list__dropdown" <Button
type="select" key={type}
name="trending_sort" button="alt"
value={typeSort} onClick={() => handleTypeSort(type)}
onChange={e => handleTypeSort(e.target.value)} className={classnames(`button-toggle button-toggle--${type}`, {
> 'button-toggle--active': typeSort === type,
{SEARCH_TYPES.map(type => ( })}
<option key={type} value={type}> icon={toCapitalCase(type)}
{__(toCapitalCase(type))} label={__(toCapitalCase(type))}
</option> />
))} ))}
</FormField>
{!hideCustomization && (
<Fragment>
<span className="claim-list__conjuction">{__('for')}</span>
{!personalView && tags && tags.length ? (
tags.map(tag => <Tag key={tag} name={tag} disabled type="large" />)
) : (
<FormField
type="select"
name="trending_overview"
className="claim-list__dropdown"
value={personalSort}
onChange={e => {
handlePersonalSort(e.target.value);
}}
>
{SEARCH_FILTER_TYPES.map(type => (
<option key={type} value={type}>
{type === SEARCH_SORT_ALL
? __('Everyone')
: type === SEARCH_SORT_YOU
? __('Tags You Follow')
: __('Channels You Follow')}
</option>
))}
</FormField>
)}
</Fragment>
)}
{typeSort === 'top' && ( {typeSort === 'top' && (
<FormField <FormField
className="claim-list__dropdown" className="claim-list__dropdown"
@ -308,7 +231,7 @@ function ClaimListDiscover(props: Props) {
onScrollBottom={handleScrollBottom} onScrollBottom={handleScrollBottom}
page={page} page={page}
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
empty={emptyState} empty={noResults}
/> />
{loading && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)} {loading && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}

View file

@ -329,4 +329,37 @@ export const icons = {
<line x1="3" y1="18" x2="21" y2="18" /> <line x1="3" y1="18" x2="21" y2="18" />
</g> </g>
), ),
[ICONS.DISCOVER]: buildIcon(
<g>
<circle cx="12" cy="12" r="10" />
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" />
</g>
),
[ICONS.TRENDING]: buildIcon(
<g>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
<polyline points="17 6 23 6 23 12" />
</g>
),
[ICONS.TOP]: buildIcon(
<g>
<line x1="12" y1="20" x2="12" y2="10" />
<line x1="18" y1="20" x2="18" y2="4" />
<line x1="6" y1="20" x2="6" y2="16" />
</g>
),
[ICONS.NEW]: buildIcon(
<g>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</g>
),
[ICONS.INVITE]: buildIcon(
<g>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</g>
),
}; };

View file

@ -127,33 +127,42 @@ const Header = (props: Props) => {
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}> <div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}>
{(!IS_WEB || authenticated) && ( {(!IS_WEB || authenticated) && (
<Fragment> <Fragment>
<Menu> <Button
<MenuButton className="header__navigation-item menu__title">{getWalletTitle()}</MenuButton> navigate={`/$/${PAGES.WALLET}`}
<MenuList className="menu__list--header"> className="header__navigation-item menu__title header__navigation-item--balance"
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.WALLET}`)}> label={getWalletTitle()}
<Icon aria-hidden icon={ICONS.WALLET} /> />
{__('Wallet')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')}
</MenuItem>
</MenuList>
</Menu>
<Menu> <Menu>
<MenuButton className="header__navigation-item menu__title header__navigation-item--icon"> <MenuButton className="header__navigation-item menu__title header__navigation-item--icon">
<Icon size={18} icon={ICONS.ACCOUNT} /> <Icon size={18} icon={ICONS.ACCOUNT} />
</MenuButton> </MenuButton>
<MenuList className="menu__list--header"> <MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.ACCOUNT}`)}>
<Icon aria-hidden icon={ICONS.OVERVIEW} />
{__('Overview')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} /> <Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publish')} {__('New Publish')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISHED}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Your Publishes')}
</MenuItem>
{/* @if TARGET='app' */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.LIBRARY} />
{__('Your Library')}
</MenuItem>
{/* @endif */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.INVITE}`)}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invite A Friend')}
</MenuItem>
{authenticated ? ( {authenticated ? (
<MenuItem className="menu__link" onSelect={signOut}> <MenuItem className="menu__link" onSelect={signOut}>
<Icon aria-hidden icon={ICONS.SIGN_OUT} /> <Icon aria-hidden icon={ICONS.SIGN_OUT} />

View file

@ -17,14 +17,14 @@ type Props = {
function Page(props: Props) { function Page(props: Props) {
const { children, className, authPage = false, authenticated } = props; const { children, className, authPage = false, authenticated } = props;
const obscureSideBar = IS_WEB ? !authenticated : false; const obscureSideNavigation = IS_WEB ? !authenticated : false;
return ( return (
<Fragment> <Fragment>
<Header authHeader={authPage} /> <Header authHeader={authPage} />
<div className={classnames('main-wrapper__inner')}> <div className={classnames('main-wrapper__inner')}>
<main className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage })}>{children}</main> <main className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage })}>{children}</main>
{!authPage && <SideNavigation obscureSideBar={obscureSideBar} />} {!authPage && <SideNavigation obscureSideNavigation={obscureSideNavigation} />}
</div> </div>
</Fragment> </Fragment>
); );

View file

@ -5,10 +5,10 @@ import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
import SettingsPage from 'page/settings'; import SettingsPage from 'page/settings';
import HelpPage from 'page/help'; import HelpPage from 'page/help';
import ReportPage from 'page/report'; import ReportPage from 'page/report';
import AccountPage from 'page/account';
import ShowPage from 'page/show'; import ShowPage from 'page/show';
import PublishPage from 'page/publish'; import PublishPage from 'page/publish';
import DiscoverPage from 'page/discover'; import DiscoverPage from 'page/discover';
// import HomePage from 'page/home';
import RewardsPage from 'page/rewards'; import RewardsPage from 'page/rewards';
import FileListDownloaded from 'page/fileListDownloaded'; import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished'; import FileListPublished from 'page/fileListPublished';
@ -18,6 +18,9 @@ import SearchPage from 'page/search';
import LibraryPage from 'page/library'; import LibraryPage from 'page/library';
import WalletPage from 'page/wallet'; import WalletPage from 'page/wallet';
import TagsPage from 'page/tags'; import TagsPage from 'page/tags';
import TagsFollowingPage from 'page/tagsFollowing';
import ChannelsFollowingPage from 'page/channelsFollowing';
import ChannelsFollowingManagePage from 'page/channelsFollowingManage';
import FollowingPage from 'page/following'; import FollowingPage from 'page/following';
import ListBlockedPage from 'page/listBlocked'; import ListBlockedPage from 'page/listBlocked';
import FourOhFourPage from 'page/fourOhFour'; import FourOhFourPage from 'page/fourOhFour';
@ -70,10 +73,13 @@ function AppRouter(props: Props) {
return ( return (
<Switch> <Switch>
<Route path="/" exact component={DiscoverPage} /> {/* <Route path={`/$/${PAGES.HOME}`} exact component={HomePage} /> */}
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} /> <Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} /> <Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} /> <Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.TAGS_FOLLOWING}`} exact component={TagsFollowingPage} />
<Route path={`/$/${PAGES.CHANNELS_FOLLOWING}`} exact component={ChannelsFollowingPage} />
<Route path={`/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`} exact component={ChannelsFollowingManagePage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} /> <Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.AUTH_VERIFY}`} exact component={SignInVerifyPage} /> <Route path={`/$/${PAGES.AUTH_VERIFY}`} exact component={SignInVerifyPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} /> <Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
@ -87,7 +93,6 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} component={RewardsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} component={RewardsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TRANSACTIONS}`} component={TransactionHistoryPage} /> <PrivateRoute {...props} path={`/$/${PAGES.TRANSACTIONS}`} component={TransactionHistoryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.ACCOUNT}`} component={AccountPage} />
<PrivateRoute {...props} path={`/$/${PAGES.FOLLOWING}`} component={FollowingPage} /> <PrivateRoute {...props} path={`/$/${PAGES.FOLLOWING}`} component={FollowingPage} />
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} /> <PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />

View file

@ -2,6 +2,7 @@
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import { withRouter } from 'react-router';
import Button from 'component/button'; import Button from 'component/button';
import Tag from 'component/tag'; import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext'; import StickyBox from 'react-sticky-box/dist/esnext';
@ -11,24 +12,27 @@ type Props = {
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
followedTags: Array<Tag>, followedTags: Array<Tag>,
email: ?string, email: ?string,
obscureSideBar: boolean, obscureSideNavigation: boolean,
uploadCount: number, uploadCount: number,
sticky: boolean, sticky: boolean,
expanded: boolean, expanded: boolean,
doSignOut: () => void, doSignOut: () => void,
location: { pathname: string },
}; };
function SideBar(props: Props) { function SideNavigation(props: Props) {
const { const {
subscriptions, subscriptions,
followedTags, followedTags,
obscureSideBar, obscureSideNavigation,
uploadCount, uploadCount,
doSignOut, doSignOut,
email, email,
sticky = true, sticky = true,
expanded = false, expanded = false,
location,
} = props; } = props;
const { pathname } = location;
const isAuthenticated = Boolean(email); const isAuthenticated = Boolean(email);
function buildLink(path, label, icon, onClick) { function buildLink(path, label, icon, onClick) {
@ -49,7 +53,7 @@ function SideBar(props: Props) {
<div>{children}</div> <div>{children}</div>
); );
return obscureSideBar ? ( return obscureSideNavigation ? (
<Wrapper> <Wrapper>
<div className="card navigation--placeholder"> <div className="card navigation--placeholder">
<div className="wrap"> <div className="wrap">
@ -64,29 +68,13 @@ function SideBar(props: Props) {
<ul className="navigation-links"> <ul className="navigation-links">
{[ {[
{ {
...buildLink(null, __('Home'), ICONS.HOME), ...buildLink(PAGES.CHANNELS_FOLLOWING, __('Following'), ICONS.SUBSCRIBE),
},
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
}, },
{ {
...buildLink( ...buildLink(PAGES.TAGS_FOLLOWING, __('Your Tags'), ICONS.TAG),
PAGES.PUBLISHED, },
uploadCount ? ( {
<span> ...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER),
{__('Publishes')}
<Spinner type="small" />
</span>
) : (
__('Publishes')
),
ICONS.PUBLISH
),
}, },
].map(linkProps => ( ].map(linkProps => (
<li key={linkProps.navigate}> <li key={linkProps.navigate}>
@ -96,6 +84,28 @@ function SideBar(props: Props) {
{expanded && {expanded &&
[ [
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
},
{
...buildLink(
PAGES.PUBLISHED,
uploadCount ? (
<span>
{__('Publishes')}
<Spinner type="small" />
</span>
) : (
__('Publishes')
),
ICONS.PUBLISH
),
},
{ {
...buildLink(PAGES.WALLET, __('Wallet'), ICONS.WALLET), ...buildLink(PAGES.WALLET, __('Wallet'), ICONS.WALLET),
}, },
@ -126,27 +136,20 @@ function SideBar(props: Props) {
</li> </li>
)) ))
)} )}
<li>
<Button
navigate={`/$/${PAGES.FOLLOWING}`}
label={__('Customize')}
icon={ICONS.EDIT}
className="navigation-link"
activeClass="navigation-link--active"
/>
</li>
</ul> </ul>
<section className="navigation-links__inline"> {pathname.includes(PAGES.TAGS_FOLLOWING) && (
<ul className="navigation-links--small tags--vertical"> <ul className="navigation__secondary navigation-links--small tags--vertical">
{followedTags.map(({ name }, key) => ( {followedTags.map(({ name }, key) => (
<li className="navigation-link__wrapper" key={name}> <li className="navigation-link__wrapper" key={name}>
<Tag navigate={`/$/tags?t${name}`} name={name} /> <Tag navigate={`/$/tags?t${name}`} name={name} />
</li> </li>
))} ))}
</ul> </ul>
<ul className="navigation-links--small"> )}
{pathname.includes(PAGES.CHANNELS_FOLLOWING) && (
<ul className="navigation__secondary navigation-links--small">
{subscriptions.map(({ uri, channelName }, index) => ( {subscriptions.map(({ uri, channelName }, index) => (
<li key={uri} className="navigation-link__wrapper"> <li key={uri} className="navigation-link__wrapper">
<Button <Button
@ -158,10 +161,10 @@ function SideBar(props: Props) {
</li> </li>
))} ))}
</ul> </ul>
</section> )}
</nav> </nav>
</Wrapper> </Wrapper>
); );
} }
export default SideBar; export default withRouter(SideNavigation);

View file

@ -78,3 +78,6 @@ export const EYE = 'Eye';
export const EYE_OFF = 'EyeOff'; export const EYE_OFF = 'EyeOff';
export const SIGN_OUT = 'SignOut'; export const SIGN_OUT = 'SignOut';
export const SIGN_IN = 'SignIn'; export const SIGN_IN = 'SignIn';
export const TRENDING = 'Trending';
export const TOP = 'Top';
export const NEW = 'New';

View file

@ -3,6 +3,7 @@ exports.AUTH_VERIFY = 'verify';
exports.BACKUP = 'backup'; exports.BACKUP = 'backup';
exports.CHANNEL = 'channel'; exports.CHANNEL = 'channel';
exports.DISCOVER = 'discover'; exports.DISCOVER = 'discover';
exports.HOME = 'home';
exports.DOWNLOADED = 'downloaded'; exports.DOWNLOADED = 'downloaded';
exports.HELP = 'help'; exports.HELP = 'help';
exports.LIBRARY = 'library'; exports.LIBRARY = 'library';
@ -16,10 +17,13 @@ exports.SEND = 'send';
exports.SETTINGS = 'settings'; exports.SETTINGS = 'settings';
exports.SHOW = 'show'; exports.SHOW = 'show';
exports.ACCOUNT = 'account'; exports.ACCOUNT = 'account';
exports.FOLLOWING = 'following';
exports.SEARCH = 'search'; exports.SEARCH = 'search';
exports.TRANSACTIONS = 'transactions'; exports.TRANSACTIONS = 'transactions';
exports.TAGS = 'tags'; exports.TAGS = 'tags';
exports.TAGS_FOLLOWING = 'following/tags';
exports.CHANNELS_FOLLOWING = 'following/channels';
exports.CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage';
exports.FOLLOWING = 'following';
exports.WALLET = 'wallet'; exports.WALLET = 'wallet';
exports.BLOCKED = 'blocked'; exports.BLOCKED = 'blocked';
exports.CHANNELS = 'channels'; exports.CHANNELS = 'channels';

View file

@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectSubscriptions, selectSuggestedChannels } from 'redux/selectors/subscriptions';
import { doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
import DiscoverPage from './view';
const select = state => ({
subscribedChannels: selectSubscriptions(state),
email: selectUserVerifiedEmail(state),
suggestedSubscriptions: selectSuggestedChannels(state),
});
const perform = {
doFetchRecommendedSubscriptions,
};
export default connect(
select,
perform
)(DiscoverPage);

View file

@ -0,0 +1,56 @@
// @flow
import * as PAGES from 'constants/pages';
import React from 'react';
import ClaimListDiscover from 'component/claimListDiscover';
import ClaimList from 'component/claimList';
import Page from 'component/page';
import Button from 'component/button';
import { TYPE_NEW } from 'component/claimListDiscover/view';
type Props = {
email: string,
subscribedChannels: Array<Subscription>,
doFetchRecommendedSubscriptions: () => void,
suggestedSubscriptions: Array<{ uri: string }>,
};
function ChannelsFollowing(props: Props) {
const { subscribedChannels, suggestedSubscriptions, doFetchRecommendedSubscriptions } = props;
const hasSubsribedChannels = subscribedChannels.length > 0;
const [showTab, setShowTab] = React.useState(!hasSubsribedChannels);
React.useEffect(() => {
if (!hasSubsribedChannels) {
doFetchRecommendedSubscriptions();
}
}, [doFetchRecommendedSubscriptions, hasSubsribedChannels]);
return (
<Page>
{showTab ? (
<div className="card">
<ClaimList
header={__('Find Channels To Follow')}
headerAltControls={
<Button
button="link"
label={hasSubsribedChannels && __('View Your Feed')}
onClick={() => setShowTab(false)}
/>
}
uris={suggestedSubscriptions.map(sub => `lbry://${sub.uri}`)}
/>
</div>
) : (
<ClaimListDiscover
defaultTypeSort={TYPE_NEW}
channelIds={subscribedChannels.map(sub => sub.uri.split('#')[1])}
meta={<Button button="link" label={__('Manage')} navigate={`/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`} />}
/>
)}
</Page>
);
}
export default ChannelsFollowing;

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { selectSubscriptions, selectSuggestedChannels } from 'redux/selectors/subscriptions';
import { doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
import ChannelsFollowingManagePage from './view';
const select = state => ({
subscribedChannels: selectSubscriptions(state),
suggestedSubscriptions: selectSuggestedChannels(state),
});
export default connect(
select,
{
doFetchRecommendedSubscriptions,
}
)(ChannelsFollowingManagePage);

View file

@ -0,0 +1,57 @@
// @flow
import * as PAGES from 'constants/pages';
import React, { useEffect } from 'react';
import Page from 'component/page';
import ClaimList from 'component/claimList';
import Button from 'component/button';
type Props = {
subscribedChannels: Array<Subscription>,
location: { search: string },
history: { push: string => void },
doFetchRecommendedSubscriptions: () => void,
suggestedSubscriptions: Array<{ uri: string }>,
};
function ChannelsFollowingManagePage(props: Props) {
const { subscribedChannels, location, history, doFetchRecommendedSubscriptions, suggestedSubscriptions } = props;
const hasSubscriptions = !!subscribedChannels.length;
const channelUris = subscribedChannels.map(({ uri }) => uri);
const { search } = location;
const urlParams = new URLSearchParams(search);
const viewingSuggestedSubs = urlParams.get('view') || !hasSubscriptions;
function onClick() {
let url = `/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`;
if (!viewingSuggestedSubs) {
url += '?view=discover';
}
history.push(url);
}
useEffect(() => {
doFetchRecommendedSubscriptions();
}, [doFetchRecommendedSubscriptions]);
return (
<Page>
<div className="card">
<ClaimList
header={viewingSuggestedSubs ? __('Discover New Channels') : __('Channels You Follow')}
headerAltControls={
<Button
button="link"
label={viewingSuggestedSubs ? hasSubscriptions && __('View Your Channels') : __('Find New Channels')}
onClick={() => onClick()}
/>
}
uris={viewingSuggestedSubs ? suggestedSubscriptions.map(sub => `lbry://${sub.uri}`) : channelUris}
/>
</div>
</Page>
);
}
export default ChannelsFollowingManagePage;

View file

@ -1,28 +1,12 @@
// @flow // @flow
import * as PAGES from 'constants/pages';
import React from 'react'; import React from 'react';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import TagsSelect from 'component/tagsSelect';
import Page from 'component/page'; import Page from 'component/page';
import Button from 'component/button';
type Props = {
followedTags: Array<Tag>,
email: string,
};
function DiscoverPage(props: Props) {
const { followedTags, email } = props;
function DiscoverPage() {
return ( return (
<Page> <Page>
{(email || !IS_WEB) && <TagsSelect showClose title={__('Customize Your Homepage')} />} <ClaimListDiscover />
<ClaimListDiscover
hideCustomization={IS_WEB && !email}
personalView
tags={followedTags.map(tag => tag.name)}
meta={<Button button="link" label={__('Customize')} requiresAuth={IS_WEB} navigate={`/$/${PAGES.FOLLOWING}`} />}
/>
</Page> </Page>
); );
} }

18
ui/page/home/index.js Normal file
View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import DiscoverPage from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
subscribedChannels: selectSubscriptions(state),
email: selectUserVerifiedEmail(state),
});
const perform = {};
export default connect(
select,
perform
)(DiscoverPage);

22
ui/page/home/view.jsx Normal file
View file

@ -0,0 +1,22 @@
// @flow
import React from 'react';
import Page from 'component/page';
// import Button from 'component/button';
// type Props = {};
function HomePage() {
// props: Props
// const {} = props;
return (
<Page>
{/* TODO */}
{/* Recent From Following */}
{/* Trending for your tags */}
{/* Trending for everyone */}
</Page>
);
}
export default HomePage;

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import DiscoverPage from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
subscribedChannels: selectSubscriptions(state),
email: selectUserVerifiedEmail(state),
});
const perform = {};
export default connect(
select,
perform
)(DiscoverPage);

View file

@ -0,0 +1,30 @@
// @flow
import * as PAGES from 'constants/pages';
import React from 'react';
import ClaimListDiscover from 'component/claimListDiscover';
import TagsSelect from 'component/tagsSelect';
import Page from 'component/page';
import Button from 'component/button';
type Props = {
followedTags: Array<Tag>,
email: string,
};
function DiscoverPage(props: Props) {
const { followedTags, email } = props;
return (
<Page>
{(email || !IS_WEB) && <TagsSelect showClose title={__('Find New Tags To Follow')} />}
<ClaimListDiscover
hideCustomization={IS_WEB && !email}
personalView
tags={followedTags.map(tag => tag.name)}
meta={<Button button="link" label={__('Customize')} requiresAuth={IS_WEB} navigate={`/$/${PAGES.FOLLOWING}`} />}
/>
</Page>
);
}
export default DiscoverPage;

View file

@ -467,6 +467,7 @@ export function doGetAndPopulatePreferences() {
); );
}); });
} }
doPreferenceGet('shared', successCb, failCb); doPreferenceGet('shared', successCb, failCb);
}; };
} }

View file

@ -64,3 +64,42 @@ svg + .button__label,
.button__label + svg { .button__label + svg {
margin-left: var(--spacing-small); margin-left: var(--spacing-small);
} }
.button-toggle {
padding: 0 var(--spacing-medium);
height: var(--height-button);
font-size: var(--font-base);
border: 1px solid var(--color-border);
border-left-width: 0;
border-radius: 0;
margin: 0;
svg {
opacity: 0.5;
}
&:first-of-type {
border-left-width: 1px;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
&:last-of-type {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
}
.button-toggle--active {
color: var(--color-primary);
background-color: var(--color-primary-alt);
svg {
opacity: 1;
}
&:hover {
cursor: default;
text-decoration: none;
}
}

View file

@ -7,7 +7,7 @@
.claim-list__header { .claim-list__header {
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 4.5rem; min-height: 6rem;
padding: var(--spacing-medium); padding: var(--spacing-medium);
font-size: var(--font-body); font-size: var(--font-body);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
@ -15,10 +15,6 @@
border-top-left-radius: var(--card-radius); border-top-left-radius: var(--card-radius);
border-top-right-radius: var(--card-radius); border-top-right-radius: var(--card-radius);
& > *:not(:last-child) {
margin-right: 0.5rem;
}
fieldset-section { fieldset-section {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
@ -34,6 +30,7 @@
margin-bottom: 0; margin-bottom: 0;
padding: 0 var(--spacing-medium); padding: 0 var(--spacing-medium);
padding-right: var(--spacing-large); padding-right: var(--spacing-large);
margin-left: var(--spacing-medium);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
font-size: var(--font-small); font-size: var(--font-small);
@ -42,6 +39,10 @@
} }
} }
.claim-list__header-title {
display: block;
}
.claim-list__conjuction { .claim-list__conjuction {
color: var(--color-text-subtitle); color: var(--color-text-subtitle);
font-size: var(--font-small); font-size: var(--font-small);

View file

@ -47,6 +47,7 @@
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
height: var(--height-input);
} }
.header__menu { .header__menu {
@ -113,7 +114,18 @@
.header__navigation-item--back, .header__navigation-item--back,
.header__navigation-item--forward, .header__navigation-item--forward,
.header__navigation-item--icon { .header__navigation-item--icon {
width: 3rem; width: var(--height-button);
background-color: var(--color-header-button);
border-radius: 1.5rem;
margin-left: var(--spacing-small);
svg {
stroke: var(--color-text);
}
&:hover {
background-color: var(--color-primary-alt);
}
} }
.header__navigation-item--lbry { .header__navigation-item--lbry {
@ -125,3 +137,7 @@
width: 2rem; width: 2rem;
} }
} }
.header__navigation-item--balance {
margin: 0 var(--spacing-medium);
}

View file

@ -13,7 +13,7 @@
.modal { .modal {
@extend .card; @extend .card;
background-color: var(--color-card-background); background-color: var(--color-modal-background);
line-height: 1.55; line-height: 1.55;
min-width: 500px; min-width: 500px;
max-width: var(--modal-width); max-width: var(--modal-width);

View file

@ -7,6 +7,10 @@
} }
} }
.navigation__secondary {
margin-top: var(--spacing-large);
}
.navigation--placeholder { .navigation--placeholder {
@extend .navigation; @extend .navigation;
padding: 2rem 1.5rem; padding: 2rem 1.5rem;

View file

@ -25,8 +25,6 @@
@extend .tags; @extend .tags;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
margin: 0;
margin-top: var(--spacing-small);
li:last-child .tag { li:last-child .tag {
margin-bottom: 0; margin-bottom: 0;

View file

@ -7,6 +7,7 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
font-size: var(--font-small); font-size: var(--font-small);
height: var(--height-input);
> .icon { > .icon {
top: 0; top: 0;

View file

@ -18,10 +18,6 @@
outline: none; outline: none;
background-color: var(--color-menu-background); background-color: var(--color-menu-background);
border-top: none; border-top: none;
&:focus {
box-shadow: none;
}
} }
[data-reach-menu-item] { [data-reach-menu-item] {

View file

@ -1,6 +1,13 @@
// Generic html styles used across the App // Generic html styles used across the App
// component specific styling should go in the component scss file // component specific styling should go in the component scss file
.top-bid {
width: 100%;
height: 400px;
background-color: var(--color-primary-alt);
padding: 1rem;
}
*::selection { *::selection {
background-color: var(--color-text-selection-bg); background-color: var(--color-text-selection-bg);
color: var(--color-text-selection); color: var(--color-text-selection);

View file

@ -5,11 +5,11 @@
/* #5a6570; - 25% */ /* #5a6570; - 25% */
[theme='dark'] { [theme='dark'] {
--color-primary-alt: #30574e; --color-primary-alt: #3e675d;
--color-primary: #60e1ba; --color-primary: #74dfbf;
// Button // Button
--color-link: #38d9a9; --color-link: var(--color-primary);
--color-link-hover: #60e1ba; --color-link-hover: #60e1ba;
--color-link-active: #60e1ba; --color-link-active: #60e1ba;
--color-link-icon: #89939e; --color-link-icon: #89939e;
@ -21,6 +21,7 @@
--color-button-secondary-bg: #395877; --color-button-secondary-bg: #395877;
--color-button-secondary-bg-hover: #4b6d8f; --color-button-secondary-bg-hover: #4b6d8f;
--color-button-secondary-text: #a3c1e0; --color-button-secondary-text: #a3c1e0;
--color-header-button: var(--color-link-icon);
// Color // Color
--color-focus: #2d69a5; --color-focus: #2d69a5;
@ -34,6 +35,7 @@
--color-tab-text: var(--color-white); --color-tab-text: var(--color-white);
--color-tabs-background: #434b53; --color-tabs-background: #434b53;
--color-tab-divider: var(--color-white); --color-tab-divider: var(--color-white);
--color-modal-background: var(--color-header-background);
// Text // Text
--color-text: #eeeeee; --color-text: #eeeeee;
@ -49,7 +51,7 @@
--color-input: #f4f4f5; --color-input: #f4f4f5;
--color-input-label: #d4d4d4; --color-input-label: #d4d4d4;
--color-input-placeholder: #f4f4f5; --color-input-placeholder: #f4f4f5;
--color-input-bg: #7d8894; --color-input-bg: #5d6772;
--color-input-bg-copyable: #434b53; --color-input-bg-copyable: #434b53;
--color-input-border: var(--color-border); --color-input-border: var(--color-border);
--color-input-border-active: var(--color-secondary); --color-input-border-active: var(--color-secondary);

View file

@ -1,8 +1,9 @@
:root { :root {
// Button // Button
--color-link-icon: var(--color-gray-5); --color-link-icon: var(--color-gray-4);
--color-link-active: var(--color-primary); --color-link-active: var(--color-primary);
--color-navigation-link: var(--color-link-icon); --color-navigation-link: var(--color-gray-5);
--color-header-button: #f7f7f7;
// Color // Color
--color-background: #f7f7f7; --color-background: #f7f7f7;
@ -34,6 +35,7 @@
--color-file-viewer-background: var(--color-card-background); --color-file-viewer-background: var(--color-card-background);
--color-tabs-background: var(--color-secondary-alt); --color-tabs-background: var(--color-secondary-alt);
--color-tab-divider: var(--color-secondary); --color-tab-divider: var(--color-secondary);
--color-modal-background: var(--color-card-background);
// Menu // Menu
--color-menu-background: var(--color-header-background); --color-menu-background: var(--color-header-background);