mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Make mobile the default mode.
Summary: - Make unsupported browser warning into an unobtrusive one-liner, similar in style to notifications. - Move browser warning details into a support page, linked from "Learn more" link. - Show different mobile and desktop warnings. - Once dismissed, remember dismissal for a year rather than just for the session. - Turn the Sign-In button (for anon users) into a menu (for the sake of exposing the Toggle Mobile Mode option) - Improve styling of HomeIntro screens when on small screen. - Flip the default for setting mobile viewport to true Test Plan: Added minor unittest for localStorageBoolObs; fixed other affected tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2738
This commit is contained in:
parent
31ffd21b4e
commit
d8d1a91beb
@ -131,39 +131,58 @@ div.dev_warning {
|
||||
#browser-check-problem {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 5000;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding-top: 3em;
|
||||
padding: 4px;
|
||||
|
||||
/* Copy common styles that are normally set from JS-generated CSS */
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
#browser-check-problem div.browser-check-wrapper {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
width: 100%;
|
||||
}
|
||||
#browser-check-problem div.browser-check-message, #browser-check-problem div.browser-check-options {
|
||||
margin: 0 auto;
|
||||
max-width: 400px;
|
||||
padding: 1em;
|
||||
background: white;
|
||||
}
|
||||
#browser-check-problem div.browser-check-options {
|
||||
text-align: center;
|
||||
}
|
||||
#browser-check-problem a {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
color: #16B378;
|
||||
border: 1px solid #16B378;
|
||||
.browser-check-wrapper {
|
||||
margin: auto;
|
||||
max-width: 600px;
|
||||
padding: 8px 24px;
|
||||
background-color: #040404;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 0 4px 0 white;
|
||||
}
|
||||
#browser-check-problem a:hover {
|
||||
text-decoration: none;
|
||||
color: #009058;
|
||||
border: 1px solid #009058;
|
||||
.browser-check-wrapper td {
|
||||
vertical-align: middle;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.browser-check-mobile {
|
||||
display: none;
|
||||
}
|
||||
.browser-check-is-mobile .browser-check-mobile {
|
||||
display: inline;
|
||||
}
|
||||
.browser-check-is-mobile .browser-check-desktop {
|
||||
display: none;
|
||||
}
|
||||
.browser-check-wrapper a {
|
||||
color: #16B378;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.browser-check-wrapper a:hover {
|
||||
color: #b1ffe2;
|
||||
}
|
||||
|
||||
.browser-check-close {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #009058;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.browser-check-close:hover {
|
||||
background-color: #16B378;
|
||||
}
|
||||
|
@ -48,11 +48,15 @@ function createStorage(): Storage {
|
||||
|
||||
/**
|
||||
* Helper to create a boolean observable whose state is stored in localStorage.
|
||||
*
|
||||
* Optionally, a default value of true will make the observable start off as true. Note that the
|
||||
* same default value should be used for an observable every time it's created.
|
||||
*/
|
||||
export function localStorageBoolObs(key: string): Observable<boolean> {
|
||||
export function localStorageBoolObs(key: string, defValue = false): Observable<boolean> {
|
||||
const store = getStorage();
|
||||
const obs = Observable.create(null, Boolean(store.getItem(key)));
|
||||
obs.addListener((val) => val ? store.setItem(key, 'true') : store.removeItem(key));
|
||||
const storedNegation = defValue ? 'false' : 'true';
|
||||
const obs = Observable.create(null, store.getItem(key) === storedNegation ? !defValue : defValue);
|
||||
obs.addListener((val) => val === defValue ? store.removeItem(key) : store.setItem(key, storedNegation));
|
||||
return obs;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {showDocSettingsModal} from 'app/client/ui/DocumentSettings';
|
||||
import {showProfileModal} from 'app/client/ui/ProfileDialog';
|
||||
import {createUserImage} from 'app/client/ui/UserImage';
|
||||
import * as viewport from 'app/client/ui/viewport';
|
||||
import {primaryButtonLink} from 'app/client/ui2018/buttons';
|
||||
import {primaryButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, mediaDeviceNotSmall, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {menu, menuDivider, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
||||
@ -39,9 +39,9 @@ export class AccountWidget extends Disposable {
|
||||
cssUserIcon(createUserImage(user, 'medium', testId('user-icon')),
|
||||
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
|
||||
) :
|
||||
primaryButtonLink('Sign in',
|
||||
{href: getLoginOrSignupUrl(), style: 'margin: 8px'},
|
||||
testId('user-signin'))
|
||||
cssSignInButton('Sign in', icon('Collapse'), testId('user-signin'),
|
||||
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
|
||||
)
|
||||
)
|
||||
),
|
||||
testId('dm-account'),
|
||||
@ -63,7 +63,7 @@ export class AccountWidget extends Disposable {
|
||||
* Renders the content of the account menu, with a list of available orgs, settings, and sign-out.
|
||||
* Note that `user` should NOT be anonymous (none of the items are really relevant).
|
||||
*/
|
||||
private _makeAccountMenu(user: FullUser): DomElementArg[] {
|
||||
private _makeAccountMenu(user: FullUser|null): DomElementArg[] {
|
||||
// Opens the user-manager for the org.
|
||||
const manageUsers = async (org: Organization) => {
|
||||
const api = this._appModel.api;
|
||||
@ -78,7 +78,31 @@ export class AccountWidget extends Disposable {
|
||||
const currentOrg = this._appModel.currentOrg;
|
||||
const gristDoc = this._docPageModel ? this._docPageModel.gristDoc.get() : null;
|
||||
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount &&
|
||||
(currentOrg.billingAccount.isManager || user.email === SUPPORT_EMAIL));
|
||||
(currentOrg.billingAccount.isManager || user?.email === SUPPORT_EMAIL));
|
||||
|
||||
// The 'Document Settings' item, when there is an open document.
|
||||
const documentSettingsItem = (gristDoc ?
|
||||
menuItem(() => showDocSettingsModal(gristDoc.docInfo, this._docPageModel!), 'Document Settings',
|
||||
testId('dm-doc-settings')) :
|
||||
null);
|
||||
|
||||
// The item to toggle mobile mode (presence of viewport meta tag).
|
||||
const mobileModeToggle = menuItem(viewport.toggleViewport,
|
||||
cssSmallDeviceOnly.cls(''), // Only show this toggle on small devices.
|
||||
'Toggle Mobile Mode',
|
||||
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
|
||||
testId('usermenu-toggle-mobile'),
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return [
|
||||
menuItemLink({href: getLoginOrSignupUrl()}, 'Sign in'),
|
||||
menuDivider(),
|
||||
documentSettingsItem,
|
||||
menuItemLink({href: commonUrls.plans}, 'Pricing'),
|
||||
mobileModeToggle,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
cssUserInfo(
|
||||
@ -89,11 +113,7 @@ export class AccountWidget extends Disposable {
|
||||
),
|
||||
menuItem(() => showProfileModal(this._appModel), 'Profile Settings'),
|
||||
|
||||
// Enable 'Document Settings' when there is an open document.
|
||||
(gristDoc ?
|
||||
menuItem(() => showDocSettingsModal(gristDoc.docInfo, this._docPageModel!), 'Document Settings',
|
||||
testId('dm-doc-settings')) :
|
||||
null),
|
||||
documentSettingsItem,
|
||||
|
||||
// Show 'Organization Settings' when on a home page of a valid org.
|
||||
(!this._docPageModel && currentOrg && !currentOrg.owner ?
|
||||
@ -112,12 +132,7 @@ export class AccountWidget extends Disposable {
|
||||
) :
|
||||
menuItemLink({href: commonUrls.plans}, 'Upgrade Plan'),
|
||||
|
||||
menuItem(viewport.toggleViewport,
|
||||
cssSmallDeviceOnly.cls(''), // Only show this toggle on small devices.
|
||||
'Toggle Mobile Mode',
|
||||
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
|
||||
testId('usermenu-toggle-mobile'),
|
||||
),
|
||||
mobileModeToggle,
|
||||
|
||||
// TODO Add section ("Here right now") listing icons of other users currently on this doc.
|
||||
// (See Invision "Panels" near the bottom.)
|
||||
@ -246,3 +261,9 @@ const cssSmallDeviceOnly = styled(menuItem, `
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const cssSignInButton = styled(primaryButton, `
|
||||
display: flex;
|
||||
margin: 8px;
|
||||
gap: 4px;
|
||||
`);
|
||||
|
@ -216,17 +216,9 @@ export const spinner = styled('div', `
|
||||
`);
|
||||
|
||||
export const prefSelectors = styled('div', `
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
right: 64px;
|
||||
float: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media ${mediaSmall} {
|
||||
& {
|
||||
right: 24px;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const sortSelector = styled('div', `
|
||||
|
@ -6,7 +6,7 @@ import {examples} from 'app/client/ui/ExampleInfo';
|
||||
import {createDocAndOpen, importDocAndOpen} from 'app/client/ui/HomeLeftPane';
|
||||
import {buildPinnedDoc} from 'app/client/ui/PinnedDocs';
|
||||
import {bigBasicButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {colors, mediaXSmall, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {cssLink} from 'app/client/ui2018/links';
|
||||
import {commonUrls} from 'app/common/gristUrls';
|
||||
@ -127,6 +127,12 @@ function buildExampleItem(doc: Document, home: HomeModel, view: 'list'|'icons')
|
||||
const cssIntroSplit = styled(css.docBlock, `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media ${mediaXSmall} {
|
||||
& {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const cssIntroLeft = styled('div', `
|
||||
@ -134,6 +140,7 @@ const cssIntroLeft = styled('div', `
|
||||
overflow: hidden;
|
||||
max-height: 150px;
|
||||
text-align: center;
|
||||
margin: 32px 0;
|
||||
`);
|
||||
|
||||
const cssIntroRight = styled('div', `
|
||||
|
@ -2,7 +2,7 @@ import {isIOS} from 'app/client/lib/browserInfo';
|
||||
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||
import {dom} from 'grainjs';
|
||||
|
||||
export const viewportEnabled = localStorageBoolObs('viewportEnabled');
|
||||
export const viewportEnabled = localStorageBoolObs('viewportEnabled', true);
|
||||
|
||||
export function toggleViewport() {
|
||||
viewportEnabled.set(!viewportEnabled.get());
|
||||
|
@ -157,10 +157,12 @@ export const testId: TestId = makeTestId('test-');
|
||||
// Min width for normal screen layout (in px). Note: <768px is bootstrap's definition of small
|
||||
// screen (covers phones, including landscape, but not tablets).
|
||||
const mediumScreenWidth = 768;
|
||||
const smallScreenWidth = 576; // Anything below this is extra-small (e.g. portrait phones).
|
||||
|
||||
// Fractional width for max-query follows https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints
|
||||
export const mediaSmall = `(max-width: ${mediumScreenWidth - 0.02}px)`;
|
||||
export const mediaNotSmall = `(min-width: ${mediumScreenWidth}px)`;
|
||||
export const mediaXSmall = `(max-width: ${smallScreenWidth - 0.02}px)`;
|
||||
|
||||
export const mediaDeviceNotSmall = `(min-device-width: ${mediumScreenWidth}px)`;
|
||||
|
||||
|
@ -39,22 +39,20 @@
|
||||
</div>
|
||||
|
||||
<div id="browser-check-problem" style="display: none;">
|
||||
<div class="browser-check-wrapper">
|
||||
<div class="browser-check-message">
|
||||
<h4>
|
||||
Grist may not work well in your browser.
|
||||
</h4>
|
||||
<p>
|
||||
The best experience is with modern Firefox or Chrome on the desktop.
|
||||
Other modern browsers will work to the degree they are standards compliant.
|
||||
</p>
|
||||
</div>
|
||||
<div class="browser-check-options">
|
||||
<a href="#" id="browser-check-problem-dismiss">Try it anyway</a>
|
||||
<a href="https://www.mozilla.org/en-US/firefox/new/">Get Firefox</a>
|
||||
<a href="https://www.google.com/chrome/">Get Chrome</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class="browser-check-wrapper"><tr>
|
||||
<td class="browser-check-message">
|
||||
<span class="browser-check-desktop">
|
||||
Grist works best on modern Firefox or Chrome.
|
||||
</span>
|
||||
<span class="browser-check-mobile">
|
||||
Grist mobile support is a work in progress.
|
||||
</span>
|
||||
<a href="https://support.getgrist.com/browser-support" target="_blank">Learn more</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="browser-check-close" id="browser-check-problem-dismiss">Dismiss</div>
|
||||
</td>
|
||||
</tr></table>
|
||||
</div>
|
||||
|
||||
<!-- INSERT CONFIG -->
|
||||
|
Loading…
Reference in New Issue
Block a user