mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Add viewport meta tag conditionally, and show a toggle for it on small devices.
Summary: - Enable narrow-screen layout for home page - Clean up margins/spacing on small-screen home page - Use "<768" as small-screen condition rather than "<=768". - Include meta-viewport tag conditionally, off by default. - Include "Toggle Mobile Mode" option in AccountMenu to toggle it on. - In a test, add an after() clause to restore window size even when test fails Test Plan: Only tested manually on iPhone (Safari & FF). Reviewers: paulfitz Reviewed By: paulfitz Subscribers: cyprien Differential Revision: https://phab.getgrist.com/D2708
This commit is contained in:
		
							parent
							
								
									f4366a01b3
								
							
						
					
					
						commit
						586b6568af
					
				@ -1,11 +1,58 @@
 | 
			
		||||
import {Observable} from 'grainjs';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns true if storage is functional. In some cass (e.g. when embedded), localStorage may
 | 
			
		||||
 * throw errors. If so, we return false. This implementation is the approach taken by store.js.
 | 
			
		||||
 */
 | 
			
		||||
function testStorage(storage: Storage) {
 | 
			
		||||
  try {
 | 
			
		||||
    const testStr = '__localStorage_test';
 | 
			
		||||
    storage.setItem(testStr, testStr);
 | 
			
		||||
    const ok = (storage.getItem(testStr) === testStr);
 | 
			
		||||
    storage.removeItem(testStr);
 | 
			
		||||
    return ok;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns localStorage if functional, or sessionStorage, or an in-memory storage. The fallbacks
 | 
			
		||||
 * help with tests, and may help when Grist is embedded.
 | 
			
		||||
 */
 | 
			
		||||
export function getStorage(): Storage {
 | 
			
		||||
  return _storage || (_storage = createStorage());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let _storage: Storage|undefined;
 | 
			
		||||
 | 
			
		||||
function createStorage(): Storage {
 | 
			
		||||
  if (typeof localStorage !== 'undefined' && testStorage(localStorage)) {
 | 
			
		||||
    return localStorage;
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof sessionStorage !== 'undefined' && testStorage(sessionStorage)) {
 | 
			
		||||
    return sessionStorage;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fall back to a Map-based implementation of (non-persistent) localStorage.
 | 
			
		||||
  const values = new Map<string, string>();
 | 
			
		||||
  return {
 | 
			
		||||
    setItem(key: string, val: string) { values.set(key, val); },
 | 
			
		||||
    getItem(key: string) { return values.get(key) ?? null; },
 | 
			
		||||
    removeItem(key: string) { values.delete(key); },
 | 
			
		||||
    clear() { values.clear(); },
 | 
			
		||||
    get length() { return values.size; },
 | 
			
		||||
    key(index: number): string|null { throw new Error('Not implemented'); },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper to create a boolean observable whose state is stored in localStorage.
 | 
			
		||||
 */
 | 
			
		||||
export function localStorageBoolObs(key: string): Observable<boolean> {
 | 
			
		||||
  const obs = Observable.create(null, Boolean(localStorage.getItem(key)));
 | 
			
		||||
  obs.addListener((val) => val ? localStorage.setItem(key, 'true') : localStorage.removeItem(key));
 | 
			
		||||
  const store = getStorage();
 | 
			
		||||
  const obs = Observable.create(null, Boolean(store.getItem(key)));
 | 
			
		||||
  obs.addListener((val) => val ? store.setItem(key, 'true') : store.removeItem(key));
 | 
			
		||||
  return obs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,7 +60,8 @@ export function localStorageBoolObs(key: string): Observable<boolean> {
 | 
			
		||||
 * Helper to create a string observable whose state is stored in localStorage.
 | 
			
		||||
 */
 | 
			
		||||
export function localStorageObs(key: string): Observable<string|null> {
 | 
			
		||||
  const obs = Observable.create<string|null>(null, localStorage.getItem(key));
 | 
			
		||||
  obs.addListener((val) => (val === null) ? localStorage.removeItem(key) : localStorage.setItem(key, val));
 | 
			
		||||
  const store = getStorage();
 | 
			
		||||
  const obs = Observable.create<string|null>(null, store.getItem(key));
 | 
			
		||||
  obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, val));
 | 
			
		||||
  return obs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,9 @@ import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, urlState} from 'app/clie
 | 
			
		||||
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 {colors, testId, vars} from 'app/client/ui2018/cssVars';
 | 
			
		||||
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';
 | 
			
		||||
import {commonUrls} from 'app/common/gristUrls';
 | 
			
		||||
@ -111,6 +112,13 @@ 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'),
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
      // TODO Add section ("Here right now") listing icons of other users currently on this doc.
 | 
			
		||||
      // (See Invision "Panels" near the bottom.)
 | 
			
		||||
 | 
			
		||||
@ -222,3 +230,19 @@ const cssOrgCheckmark = styled(icon, `
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
const cssCheckmark = styled(icon, `
 | 
			
		||||
  flex: none;
 | 
			
		||||
  margin-left: 16px;
 | 
			
		||||
  --icon-color: ${colors.lightGreen};
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
// Note that this css class hides the item when the device width is small (not based on viewport
 | 
			
		||||
// width, which may be larger). This only appropriate for when to enable the "mobile mode" toggle.
 | 
			
		||||
const cssSmallDeviceOnly = styled(menuItem, `
 | 
			
		||||
  @media ${mediaDeviceNotSmall} {
 | 
			
		||||
    & {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import * as koUtil from 'app/client/lib/koUtil';
 | 
			
		||||
import {reportError, TopAppModel, TopAppModelImpl} from 'app/client/models/AppModel';
 | 
			
		||||
import {setUpErrorHandling} from 'app/client/models/errors';
 | 
			
		||||
import {createAppUI} from 'app/client/ui/AppUI';
 | 
			
		||||
import {addViewportTag} from 'app/client/ui/viewport';
 | 
			
		||||
import {attachCssRootVars} from 'app/client/ui2018/cssVars';
 | 
			
		||||
import {BaseAPI} from 'app/common/BaseAPI';
 | 
			
		||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
 | 
			
		||||
@ -154,6 +155,7 @@ export class App extends DisposableWithEvents {
 | 
			
		||||
 | 
			
		||||
    // Add the cssRootVars class to enable the variables in cssVars.
 | 
			
		||||
    attachCssRootVars(this.topAppModel.productFlavor);
 | 
			
		||||
    addViewportTag();
 | 
			
		||||
    this.autoDispose(createAppUI(this.topAppModel, this));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -100,6 +100,7 @@ function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel) {
 | 
			
		||||
    },
 | 
			
		||||
    headerMain: createTopBarHome(appModel),
 | 
			
		||||
    contentMain: createDocMenu(pageModel),
 | 
			
		||||
    optimizeNarrowScreen: true,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import {transientInput} from 'app/client/ui/transientInput';
 | 
			
		||||
import {colors, vars} from 'app/client/ui2018/cssVars';
 | 
			
		||||
import {colors, mediaSmall, vars} from 'app/client/ui2018/cssVars';
 | 
			
		||||
import {icon} from 'app/client/ui2018/icons';
 | 
			
		||||
import {styled} from 'grainjs';
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,11 @@ export const docList = styled('div', `
 | 
			
		||||
    display: block;
 | 
			
		||||
    height: 64px;
 | 
			
		||||
  }
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    & {
 | 
			
		||||
      padding: 32px 24px 24px 24px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
export const docListHeader = styled('div', `
 | 
			
		||||
@ -216,6 +221,12 @@ export const prefSelectors = styled('div', `
 | 
			
		||||
  right: 64px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    & {
 | 
			
		||||
      right: 24px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
export const sortSelector = styled('div', `
 | 
			
		||||
@ -236,4 +247,9 @@ export const sortSelector = styled('div', `
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    background-color: ${colors.mediumGrey};
 | 
			
		||||
  }
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    & {
 | 
			
		||||
      margin-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
 */
 | 
			
		||||
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
 | 
			
		||||
import {transition} from 'app/client/ui/transitions';
 | 
			
		||||
import {colors, cssHideForNarrowScreen, maxNarrowScreenWidth} from 'app/client/ui2018/cssVars';
 | 
			
		||||
import {colors, cssHideForNarrowScreen, mediaNotSmall, mediaSmall} from 'app/client/ui2018/cssVars';
 | 
			
		||||
import {icon} from 'app/client/ui2018/icons';
 | 
			
		||||
import {dom, DomArg, noTestId, Observable, styled, TestId} from "grainjs";
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,6 @@ export function pagePanels(page: PageContents) {
 | 
			
		||||
 | 
			
		||||
  return [cssPageContainer(
 | 
			
		||||
    cssPageContainer.cls('-optimizeNarrowScreen', optimizeNarrowScreen),
 | 
			
		||||
    optimizeNarrowScreen ? dom.style('min-width', '240px') : null,
 | 
			
		||||
    cssLeftPane(
 | 
			
		||||
      testId('left-panel'),
 | 
			
		||||
      cssTopHeader(left.header),
 | 
			
		||||
@ -121,39 +120,36 @@ export function pagePanels(page: PageContents) {
 | 
			
		||||
        left.panelOpen.set(false);
 | 
			
		||||
        if (right) { right.panelOpen.set(false); }
 | 
			
		||||
      }),
 | 
			
		||||
      testId('overlay'))
 | 
			
		||||
      testId('overlay')
 | 
			
		||||
    ),
 | 
			
		||||
  ), (
 | 
			
		||||
    !optimizeNarrowScreen ? null :
 | 
			
		||||
      cssBottomFooter(
 | 
			
		||||
        testId('bottom-footer'),
 | 
			
		||||
        (left.hideOpener ? null :
 | 
			
		||||
         cssPanelOpenerNarrowScreenBtn(
 | 
			
		||||
           cssPanelOpenerNarrowScreen(
 | 
			
		||||
             'FieldTextbox',
 | 
			
		||||
             dom.on('click', () => {
 | 
			
		||||
               if (right) {
 | 
			
		||||
                 right.panelOpen.set(false);
 | 
			
		||||
               }
 | 
			
		||||
               toggleObs(left.panelOpen);
 | 
			
		||||
             }),
 | 
			
		||||
             testId('left-opener-ns')
 | 
			
		||||
           ),
 | 
			
		||||
           cssPanelOpenerNarrowScreenBtn.cls('-open', left.panelOpen)
 | 
			
		||||
         )
 | 
			
		||||
        cssPanelOpenerNarrowScreenBtn(
 | 
			
		||||
          cssPanelOpenerNarrowScreen(
 | 
			
		||||
            'FieldTextbox',
 | 
			
		||||
            dom.on('click', () => {
 | 
			
		||||
              right?.panelOpen.set(false);
 | 
			
		||||
              toggleObs(left.panelOpen);
 | 
			
		||||
            }),
 | 
			
		||||
            testId('left-opener-ns')
 | 
			
		||||
          ),
 | 
			
		||||
          cssPanelOpenerNarrowScreenBtn.cls('-open', left.panelOpen)
 | 
			
		||||
        ),
 | 
			
		||||
        page.contentBottom,
 | 
			
		||||
        (!right || right.hideOpener ? null :
 | 
			
		||||
         cssPanelOpenerNarrowScreenBtn(
 | 
			
		||||
           cssPanelOpenerNarrowScreen(
 | 
			
		||||
             'Settings',
 | 
			
		||||
             dom.on('click', () => {
 | 
			
		||||
               left.panelOpen.set(false);
 | 
			
		||||
               toggleObs(right.panelOpen);
 | 
			
		||||
             }),
 | 
			
		||||
             testId('right-opener-ns')
 | 
			
		||||
           ),
 | 
			
		||||
           cssPanelOpenerNarrowScreenBtn.cls('-open', right.panelOpen),
 | 
			
		||||
         )
 | 
			
		||||
        (!right ? null :
 | 
			
		||||
          cssPanelOpenerNarrowScreenBtn(
 | 
			
		||||
            cssPanelOpenerNarrowScreen(
 | 
			
		||||
              'Settings',
 | 
			
		||||
              dom.on('click', () => {
 | 
			
		||||
                left.panelOpen.set(false);
 | 
			
		||||
                toggleObs(right.panelOpen);
 | 
			
		||||
              }),
 | 
			
		||||
              testId('right-opener-ns')
 | 
			
		||||
            ),
 | 
			
		||||
            cssPanelOpenerNarrowScreenBtn.cls('-open', right.panelOpen),
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
      )
 | 
			
		||||
  )];
 | 
			
		||||
@ -178,15 +174,15 @@ const cssPageContainer = styled(cssHBox, `
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  min-width: 600px
 | 
			
		||||
  min-width: 600px;
 | 
			
		||||
  background-color: ${colors.lightGrey};
 | 
			
		||||
 | 
			
		||||
  @media (max-width: ${maxNarrowScreenWidth}px) {
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    &-optimizeNarrowScreen {
 | 
			
		||||
      bottom: 48px;
 | 
			
		||||
      min-width: 240px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
`);
 | 
			
		||||
export const cssLeftPane = styled(cssVBox, `
 | 
			
		||||
  position: relative;
 | 
			
		||||
@ -195,7 +191,7 @@ export const cssLeftPane = styled(cssVBox, `
 | 
			
		||||
  margin-right: 0px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  transition: margin-right 0.4s;
 | 
			
		||||
  @media (max-width: ${maxNarrowScreenWidth}px) {
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    .${cssPageContainer.className}-optimizeNarrowScreen & {
 | 
			
		||||
      width: 0px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
@ -204,7 +200,6 @@ export const cssLeftPane = styled(cssVBox, `
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      box-shadow: 10px 0 5px rgba(0, 0, 0, 0.2);
 | 
			
		||||
      border-bottom: 1px solid ${colors.mediumGrey};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &-open {
 | 
			
		||||
@ -236,7 +231,7 @@ const cssRightPane = styled(cssVBox, `
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  transition: margin-left 0.4s;
 | 
			
		||||
  z-index: 0;
 | 
			
		||||
  @media (max-width: ${maxNarrowScreenWidth}px) {
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    .${cssPageContainer.className}-optimizeNarrowScreen & {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      z-index: 10;
 | 
			
		||||
@ -244,7 +239,6 @@ const cssRightPane = styled(cssVBox, `
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      box-shadow: -10px 0 5px rgba(0, 0, 0, 0.2);
 | 
			
		||||
      border-bottom: 1px solid ${colors.mediumGrey};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &-open {
 | 
			
		||||
@ -292,7 +286,8 @@ const cssBottomFooter = styled ('div', `
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  @media not all and (max-width: ${maxNarrowScreenWidth}px) {
 | 
			
		||||
  border-top: 1px solid ${colors.mediumGrey};
 | 
			
		||||
  @media ${mediaNotSmall} {
 | 
			
		||||
    & {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
@ -352,7 +347,7 @@ const cssContentOverlay = styled('div', `
 | 
			
		||||
  opacity: 0.5;
 | 
			
		||||
  display: none;
 | 
			
		||||
  z-index: 9;
 | 
			
		||||
  @media (max-width: ${maxNarrowScreenWidth}px) {
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    .${cssPageContainer.className}-optimizeNarrowScreen & {
 | 
			
		||||
      display: unset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								app/client/ui/viewport.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/client/ui/viewport.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
 | 
			
		||||
import {dom} from 'grainjs';
 | 
			
		||||
 | 
			
		||||
export const viewportEnabled = localStorageBoolObs('viewportEnabled');
 | 
			
		||||
 | 
			
		||||
export function toggleViewport() {
 | 
			
		||||
  viewportEnabled.set(!viewportEnabled.get());
 | 
			
		||||
  if (!viewportEnabled.get()) {
 | 
			
		||||
    // Removing the meta tag doesn't cause mobile browsers to reload automatically.
 | 
			
		||||
    location.reload();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addViewportTag() {
 | 
			
		||||
  dom.update(document.head,
 | 
			
		||||
    dom.maybe(viewportEnabled, () =>
 | 
			
		||||
      dom('meta', {name: "viewport", content: "width=device-width,initial-scale=1.0"})
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -6,7 +6,7 @@
 | 
			
		||||
 * Workspace is a clickable link and document and page names are editable labels.
 | 
			
		||||
 */
 | 
			
		||||
import { urlState } from 'app/client/models/gristUrlState';
 | 
			
		||||
import { colors, cssHideForNarrowScreen, maxNarrowScreenWidth, testId } from 'app/client/ui2018/cssVars';
 | 
			
		||||
import { colors, cssHideForNarrowScreen, mediaNotSmall, testId } from 'app/client/ui2018/cssVars';
 | 
			
		||||
import { editableLabel } from 'app/client/ui2018/editableLabel';
 | 
			
		||||
import { icon } from 'app/client/ui2018/icons';
 | 
			
		||||
import { UserOverride } from 'app/common/DocListAPI';
 | 
			
		||||
@ -55,7 +55,7 @@ const cssWorkspaceNarrowScreen = styled(icon, `
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
  background-color: ${colors.slate};
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  @media not all and (max-width: ${maxNarrowScreenWidth}px) {
 | 
			
		||||
  @media ${mediaNotSmall} {
 | 
			
		||||
    & {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -154,15 +154,22 @@ export const cssRootVars = cssBodyVars.className;
 | 
			
		||||
// class ".test-{name}". Ideally, we'd use noTestId() instead in production.
 | 
			
		||||
export const testId: TestId = makeTestId('test-');
 | 
			
		||||
 | 
			
		||||
// Max width for narrow screen layout (in px). Note: 768px is bootstrap's definition of small screen
 | 
			
		||||
export const maxNarrowScreenWidth = 768;
 | 
			
		||||
// 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;
 | 
			
		||||
 | 
			
		||||
// 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 mediaDeviceNotSmall = `(min-device-width: ${mediumScreenWidth}px)`;
 | 
			
		||||
 | 
			
		||||
export function isNarrowScreen() {
 | 
			
		||||
  return window.innerWidth <= 768;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const cssHideForNarrowScreen = styled('div', `
 | 
			
		||||
  @media (max-width: ${maxNarrowScreenWidth}px) {
 | 
			
		||||
  @media ${mediaSmall} {
 | 
			
		||||
    & {
 | 
			
		||||
      display: none !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user