From abebe812dbfa1306fb98669c42f831e904d8e983 Mon Sep 17 00:00:00 2001 From: George Gevoian Date: Thu, 16 Jun 2022 20:43:26 -0700 Subject: [PATCH] (core) Show Grist video tour after welcome questions Summary: After the welcome questions are dismissed, a video tour modal will now be displayed. The video tour is also accessible via a tool button in the left panel of the home page, as well as a text button next to the Examples & Templates header. Test Plan: Browser tests. Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3477 --- app/client/components/commandList.js | 12 ++- app/client/ui/DocMenu.ts | 28 +++--- app/client/ui/DocMenuCss.ts | 36 ++++++- app/client/ui/HomeLeftPane.ts | 5 +- app/client/ui/LeftPanelCommon.ts | 32 +++--- app/client/ui/OpenVideoTour.ts | 142 +++++++++++++++++++++++++++ app/client/ui/PagePanels.ts | 24 +++-- app/client/ui/PinnedDocs.ts | 5 + app/client/ui/Tools.ts | 2 +- app/client/ui/WelcomeQuestions.ts | 14 ++- app/client/ui2018/modals.ts | 89 ++++++++++++++--- app/common/gristUrls.ts | 1 + 12 files changed, 332 insertions(+), 58 deletions(-) create mode 100644 app/client/ui/OpenVideoTour.ts diff --git a/app/client/components/commandList.js b/app/client/components/commandList.js index b82b26e9..89ae6e47 100644 --- a/app/client/components/commandList.js +++ b/app/client/components/commandList.js @@ -102,7 +102,17 @@ exports.groups = [{ name: 'openWidgetConfiguration', keys: [], desc: 'Open Custom widget configuration screen', - } + }, + { + name: 'leftPanelOpen', + keys: [], + desc: 'Shortcut to open the left panel', + }, + { + name: 'videoTourToolsOpen', + keys: [], + desc: 'Shortcut to open video tour from home left panel', + }, ] }, { group: 'Navigation', diff --git a/app/client/ui/DocMenu.ts b/app/client/ui/DocMenu.ts index fb774f56..71f6e037 100644 --- a/app/client/ui/DocMenu.ts +++ b/app/client/ui/DocMenu.ts @@ -10,6 +10,7 @@ import {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'ap import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo'; import * as css from 'app/client/ui/DocMenuCss'; import {buildHomeIntro} from 'app/client/ui/HomeIntro'; +import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour'; import {buildUpgradeNudge} from 'app/client/ui/ProductUpgrades'; import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs'; import {shadowScroll} from 'app/client/ui/shadowScroll'; @@ -25,7 +26,7 @@ import {IHomePage} from 'app/common/gristUrls'; import {SortPref, ViewPref} from 'app/common/Prefs'; import * as roles from 'app/common/roles'; import {Document, Workspace} from 'app/common/UserAPI'; -import {Computed, computed, dom, DomContents, makeTestId, Observable, observable, styled} from 'grainjs'; +import {Computed, computed, dom, DomContents, makeTestId, Observable, observable} from 'grainjs'; import sortBy = require('lodash/sortBy'); import {buildTemplateDocs} from 'app/client/ui/TemplateDocs'; import {localStorageBoolObs} from 'app/client/lib/localStorageObs'; @@ -67,7 +68,7 @@ function createLoadedDocMenu(home: HomeModel) { const flashDocId = observable(null); return css.docList( showWelcomeQuestions(home.app.userPrefsObs), - cssDocMenu( + css.docMenu( dom.maybe(!home.app.currentFeatures.workspaces, () => [ css.docListHeader('This service is not available right now'), dom('span', '(The organization needs a paid plan)') @@ -208,15 +209,18 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) { if (templates.length === 0) { return null; } const hideTemplatesObs = localStorageBoolObs('hide-examples'); - return css.templatesDocBlock( + return css.allDocsTemplates(css.templatesDocBlock( dom.autoDispose(hideTemplatesObs), - css.templatesHeader( - 'Examples & Templates', - dom.domComputed(hideTemplatesObs, (collapsed) => - collapsed ? css.templatesHeaderIcon('Expand') : css.templatesHeaderIcon('Collapse') + css.templatesHeaderWrap( + css.templatesHeader( + 'Examples & Templates', + dom.domComputed(hideTemplatesObs, (collapsed) => + collapsed ? css.templatesHeaderIcon('Expand') : css.templatesHeaderIcon('Collapse') + ), + dom.on('click', () => hideTemplatesObs.set(!hideTemplatesObs.get())), + testId('all-docs-templates-header'), ), - dom.on('click', () => hideTemplatesObs.set(!hideTemplatesObs.get())), - testId('all-docs-templates-header'), + createVideoTourTextButton(), ), dom.maybe((use) => !use(hideTemplatesObs), () => [ buildTemplateDocs(home, templates, viewSettings), @@ -228,7 +232,7 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) { ]), css.docBlock.cls((use) => '-' + use(home.currentView)), testId('all-docs-templates'), - ); + )); }); } @@ -586,7 +590,3 @@ function shouldShowTemplates(home: HomeModel, showIntro: boolean): boolean { // Show templates for all personal orgs, and for non-personal orgs when showing intro. return isPersonalOrg || showIntro; } - -const cssDocMenu = styled('div', ` - flex-grow: 1; -`); diff --git a/app/client/ui/DocMenuCss.ts b/app/client/ui/DocMenuCss.ts index fe349422..b6047e80 100644 --- a/app/client/ui/DocMenuCss.ts +++ b/app/client/ui/DocMenuCss.ts @@ -8,6 +8,11 @@ import {bigBasicButton} from 'app/client/ui2018/buttons'; // styles, which gives it priority. import 'popweasel'; +export const docMenu = styled('div', ` + flex-grow: 1; + max-width: 100%; +`); + // The "&:after" clause forces some padding below all docs. export const docList = styled('div', ` height: 100%; @@ -33,16 +38,34 @@ export const docList = styled('div', ` } `); -export const docListHeader = styled('div', ` +const listHeader = styled('div', ` min-height: 32px; line-height: 32px; - margin-bottom: 24px; color: ${colors.dark}; font-size: ${vars.xxxlargeFontSize}; font-weight: ${vars.headerControlTextWeight}; `); -export const templatesHeader = styled(docListHeader, ` +export const docListHeader = styled(listHeader, ` + margin-bottom: 24px; +`); + +export const templatesHeaderWrap = styled('div', ` + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + margin-bottom: 24px; + + @media ${mediaSmall} { + & { + flex-direction: column; + align-items: flex-start; + } + } +`); + +export const templatesHeader = styled(listHeader, ` cursor: pointer; `); @@ -53,13 +76,18 @@ export const featuredTemplatesHeader = styled(docListHeader, ` export const otherSitesHeader = templatesHeader; +export const allDocsTemplates = styled('div', ` + display: flex; +`); + export const docBlock = styled('div', ` max-width: 550px; min-width: 300px; margin-bottom: 28px; &-icons { - max-width: unset; + max-width: max-content; + min-width: calc(min(550px, 100%)); } `); diff --git a/app/client/ui/HomeLeftPane.ts b/app/client/ui/HomeLeftPane.ts index 7eb5df0b..326966ca 100644 --- a/app/client/ui/HomeLeftPane.ts +++ b/app/client/ui/HomeLeftPane.ts @@ -6,7 +6,8 @@ import {HomeModel} from 'app/client/models/HomeModel'; import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo'; import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton'; import {docImport, importFromPlugin} from 'app/client/ui/HomeImports'; -import {cssLinkText, cssPageEntry, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon'; +import {cssLinkText, cssPageEntry, cssPageIcon, cssPageLink, cssSpacer} from 'app/client/ui/LeftPanelCommon'; +import {createVideoTourToolsButton} from 'app/client/ui/OpenVideoTour'; import {transientInput} from 'app/client/ui/transientInput'; import {colors, testId} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; @@ -111,6 +112,8 @@ export function createHomeLeftPane(leftPanelOpen: Observable, home: Hom testId('dm-trash'), ), ), + cssSpacer(), + createVideoTourToolsButton(), createHelpTools(home.app), ) ) diff --git a/app/client/ui/LeftPanelCommon.ts b/app/client/ui/LeftPanelCommon.ts index d0f5a13b..0c3a3628 100644 --- a/app/client/ui/LeftPanelCommon.ts +++ b/app/client/ui/LeftPanelCommon.ts @@ -24,28 +24,25 @@ import {dom, DomContents, Observable, styled} from 'grainjs'; * Creates the "help tools", a button/link to open HelpScout beacon, and one to open the * HelpCenter in a new tab. */ -export function createHelpTools(appModel: AppModel, spacer = true): DomContents { +export function createHelpTools(appModel: AppModel): DomContents { if (shouldHideUiElement("helpCenter")) { return []; } - return [ - spacer ? cssSpacer() : null, - cssSplitPageEntry( - cssPageEntryMain( - cssPageLink(cssPageIcon('Help'), - cssLinkText('Help Center'), - dom.cls('tour-help-center'), - dom.on('click', (ev) => beaconOpenMessage({appModel})), - testId('left-feedback'), - ), + return cssSplitPageEntry( + cssPageEntryMain( + cssPageLink(cssPageIcon('Help'), + cssLinkText('Help Center'), + dom.cls('tour-help-center'), + dom.on('click', (ev) => beaconOpenMessage({appModel})), + testId('left-feedback'), ), - cssPageEntrySmall( - cssPageLink(cssPageIcon('FieldLink'), - {href: commonUrls.help, target: '_blank'}, - ), - ) ), - ]; + cssPageEntrySmall( + cssPageLink(cssPageIcon('FieldLink'), + {href: commonUrls.help, target: '_blank'}, + ), + ), + ); } /** @@ -56,6 +53,7 @@ export function leftPanelBasic(appModel: AppModel, panelOpen: Observable !use(panelOpen)), + cssSpacer(), createHelpTools(appModel), ) ) diff --git a/app/client/ui/OpenVideoTour.ts b/app/client/ui/OpenVideoTour.ts new file mode 100644 index 00000000..28a55133 --- /dev/null +++ b/app/client/ui/OpenVideoTour.ts @@ -0,0 +1,142 @@ +import * as commands from 'app/client/components/commands'; +import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon'; +import {colors} from 'app/client/ui2018/cssVars'; +import {icon} from 'app/client/ui2018/icons'; +import {modal} from 'app/client/ui2018/modals'; +import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls'; +import {dom, makeTestId, styled} from 'grainjs'; + +const testId = makeTestId('test-video-tour-'); + +/** + * Opens a modal containing a video tour of Grist. + */ + export function openVideoTour(refElement: HTMLElement) { + return modal( + (ctl) => { + return [ + cssModal.cls(''), + cssCloseButton( + cssCloseIcon('CrossBig'), + dom.on('click', () => ctl.close()), + testId('close'), + ), + cssVideoWrap( + cssVideo( + { + src: commonUrls.videoTour, + title: 'YouTube video player', + frameborder: '0', + allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', + allowfullscreen: '', + }, + ), + ), + testId('modal'), + ]; + }, + { + refElement, + variant: 'collapsing', + } + ); +} + +/** + * Creates a text button that shows the video tour on click. + */ +export function createVideoTourTextButton(): HTMLDivElement { + const elem: HTMLDivElement = cssVideoTourTextButton( + cssVideoIcon('Video'), + 'Grist Video Tour', + dom.on('click', () => openVideoTour(elem)), + testId('text-button'), + ); + + return elem; +} + +/** + * Creates the "Video Tour" button for the "Tools" section of the left panel. + * + * Shows the video tour on click. + */ +export function createVideoTourToolsButton(): HTMLDivElement | null { + if (shouldHideUiElement('helpCenter')) { return null; } + + let iconElement: HTMLElement; + + const commandsGroup = commands.createGroup({ + videoTourToolsOpen: () => openVideoTour(iconElement), + }, null, true); + + return cssPageEntryMain( + dom.autoDispose(commandsGroup), + cssPageLink( + iconElement = cssPageIcon('Video'), + cssLinkText('Video Tour'), + dom.cls('tour-help-center'), + dom.on('click', () => openVideoTour(iconElement)), + testId('tools-button'), + ), + ); +} + +const cssModal = styled('div', ` + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + width: 100%; + max-width: 864px; +`); + +const cssVideoWrap = styled('div', ` + position: relative; + padding-bottom: 56.25%; + height: 0; +`); + +const cssVideo = styled('iframe', ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +`); + +const cssVideoTourTextButton = styled('div', ` + color: ${colors.lightGreen}; + cursor: pointer; + + &:hover { + color: ${colors.darkGreen}; + } +`); + +const cssVideoIcon = styled(icon, ` + background-color: ${colors.lightGreen}; + cursor: pointer; + margin: 0px 4px 3px 0; + + .${cssVideoTourTextButton.className}:hover > & { + background-color: ${colors.darkGreen}; + } +`); + +const cssCloseButton = styled('div', ` + align-self: flex-end; + margin: -8px; + padding: 4px; + border-radius: 4px; + cursor: pointer; + --icon-color: ${colors.slate}; + + &:hover { + background-color: ${colors.mediumGreyOpaque}; + } +`); + +const cssCloseIcon = styled(icon, ` + padding: 12px; +`); diff --git a/app/client/ui/PagePanels.ts b/app/client/ui/PagePanels.ts index fc5100b9..a105aacb 100644 --- a/app/client/ui/PagePanels.ts +++ b/app/client/ui/PagePanels.ts @@ -1,9 +1,10 @@ /** * Note that it assumes the presence of cssVars.cssRootVars on . */ +import * as commands from 'app/client/components/commands'; import {urlState} from "app/client/models/gristUrlState"; import {resizeFlexVHandle} from 'app/client/ui/resizeHandle'; -import {transition} from 'app/client/ui/transitions'; +import {transition, TransitionWatcher} from 'app/client/ui/transitions'; import {colors, cssHideForNarrowScreen, mediaNotSmall, mediaSmall} from 'app/client/ui2018/cssVars'; import {isNarrowScreenObs} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; @@ -40,6 +41,7 @@ export function pagePanels(page: PageContents) { let lastLeftOpen = left.panelOpen.get(); let lastRightOpen = right?.panelOpen.get() || false; + let leftPaneDom: HTMLElement; // When switching to mobile mode, close panels; when switching to desktop, restore the // last desktop state. @@ -60,12 +62,21 @@ export function pagePanels(page: PageContents) { } }); + const commandsGroup = commands.createGroup({ + leftPanelOpen: () => new Promise((resolve) => { + const watcher = new TransitionWatcher(leftPaneDom); + watcher.onDispose(() => resolve(undefined)); + left.panelOpen.set(true); + }), + }, null, true); + return cssPageContainer( dom.autoDispose(sub1), dom.autoDispose(sub2), + dom.autoDispose(commandsGroup), page.contentTop, cssContentMain( - cssLeftPane( + leftPaneDom = cssLeftPane( testId('left-panel'), cssTopHeader(left.header), left.content, @@ -187,6 +198,7 @@ function toggleObs(boolObs: Observable) { boolObs.set(!boolObs.get()); } +const bottomFooterHeightPx = 48; const cssVBox = styled('div', ` display: flex; flex-direction: column; @@ -207,7 +219,7 @@ const cssPageContainer = styled(cssVBox, ` @media ${mediaSmall} { & { - padding-bottom: 48px; + padding-bottom: ${bottomFooterHeightPx}px; min-width: 240px; } .interface-light & { @@ -232,7 +244,7 @@ export const cssLeftPane = styled(cssVBox, ` position: fixed; z-index: 10; top: 0; - bottom: 0; + bottom: ${bottomFooterHeightPx}px; left: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */ visibility: hidden; box-shadow: 10px 0 5px rgba(0, 0, 0, 0.2); @@ -279,7 +291,7 @@ const cssRightPane = styled(cssVBox, ` position: fixed; z-index: 10; top: 0; - bottom: 0; + bottom: ${bottomFooterHeightPx}px; right: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */ box-shadow: -10px 0 5px rgba(0, 0, 0, 0.2); visibility: hidden; @@ -324,7 +336,7 @@ const cssTopHeader = styled('div', ` } `); const cssBottomFooter = styled ('div', ` - height: 48px; + height: ${bottomFooterHeightPx}px; background-color: white; z-index: 20; display: flex; diff --git a/app/client/ui/PinnedDocs.ts b/app/client/ui/PinnedDocs.ts index 48e71d0a..09ebc9ac 100644 --- a/app/client/ui/PinnedDocs.ts +++ b/app/client/ui/PinnedDocs.ts @@ -121,6 +121,11 @@ const pinnedDocWrapper = styled('div', ` &:hover { border: 1px solid ${colors.slate}; } + + /* TODO: Specify a gap on flexbox parents of pinnedDocWrapper instead. */ + &:last-child { + margin-right: 0px; + } `); const pinnedDoc = styled('a', ` diff --git a/app/client/ui/Tools.ts b/app/client/ui/Tools.ts index 861879f1..7d7f849d 100644 --- a/app/client/ui/Tools.ts +++ b/app/client/ui/Tools.ts @@ -114,7 +114,7 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse ) ), ), - createHelpTools(docPageModel.appModel, false) + createHelpTools(docPageModel.appModel), ); } diff --git a/app/client/ui/WelcomeQuestions.ts b/app/client/ui/WelcomeQuestions.ts index 58b66931..a0a9c9ac 100644 --- a/app/client/ui/WelcomeQuestions.ts +++ b/app/client/ui/WelcomeQuestions.ts @@ -1,3 +1,4 @@ +import * as commands from 'app/client/components/commands'; import {getUserPrefObs} from 'app/client/models/UserPrefs'; import {colors, testId} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; @@ -29,9 +30,16 @@ export function showWelcomeQuestions(userPrefsObs: Observable) { {method: 'POST', body: JSON.stringify({use_cases, use_other})}); } - // Whichever way the modal is closed, don't show the questions again. (We set the value to - // undefined to remove it from the JSON prefs object entirely; it's never used again.) - owner.onDispose(() => showQuestions.set(undefined)); + + owner.onDispose(async () => { + // Whichever way the modal is closed, don't show the questions again. (We set the value to + // undefined to remove it from the JSON prefs object entirely; it's never used again.) + showQuestions.set(undefined); + + // Show the Grist video tour when the modal is closed. + await commands.allCommands.leftPanelOpen.run(); + commands.allCommands.videoTourToolsOpen.run(); + }); return { title: [cssLogo(), dom('div', 'Welcome to Grist!')], diff --git a/app/client/ui2018/modals.ts b/app/client/ui2018/modals.ts index faeacc1f..cf8ae325 100644 --- a/app/client/ui2018/modals.ts +++ b/app/client/ui2018/modals.ts @@ -1,6 +1,7 @@ import {FocusLayer} from 'app/client/lib/FocusLayer'; import {reportError} from 'app/client/models/errors'; import {cssInput} from 'app/client/ui/MakeCopyMenu'; +import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions'; import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons'; import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars'; import {loadingSpinner} from 'app/client/ui2018/loaders'; @@ -112,10 +113,25 @@ export class ModalControl extends Disposable implements IModalControl { } } -export interface IModalOptions { - noEscapeKey?: boolean; // If set, escape key does not close the dialog - noClickAway?: boolean; // If set, clicking into background does not close dialog. +/** + * The modal variant. + * + * Fade-in modals open with a fade-in background animation, and close immediately. + * + * Collapsing modals open with a expanding animation from a referenced DOM element, and + * close with a collapsing animation into the referenced element. + */ +export type IModalVariant = 'fade-in' | 'collapsing'; +export interface IModalOptions { + // The modal variant. Defaults to "fade-in". + variant?: IModalVariant; + // Required for "collapsing" variant modals. This is the anchor element for animations. + refElement?: HTMLElement; + // If set, escape key does not close the dialog. + noEscapeKey?: boolean; + // If set, clicking into background does not close dialog. + noClickAway?: boolean; // If given, call and wait for this before closing the dialog. If it returns false, don't close. // Error also prevents closing, and is reported as an unexpected error. beforeClose?: () => Promise; @@ -153,41 +169,78 @@ export function modal( createFn: (ctl: IModalControl, owner: MultiHolder) => DomElementArg, options: IModalOptions = {} ): void { + const {noEscapeKey, noClickAway, refElement = document.body, variant = 'fade-in'} = options; function doClose() { if (!modalDom.isConnected) { return; } + + variant === 'collapsing' ? collapseAndCloseModal() : closeModal(); + } + + function closeModal() { document.body.removeChild(modalDom); // Ensure we run the disposers for the DOM contained in the modal. dom.domDispose(modalDom); } + + function collapseAndCloseModal() { + const watcher = new TransitionWatcher(dialogDom); + watcher.onDispose(() => closeModal()); + modalDom.classList.add(cssModalBacker.className + '-collapsing'); + collapseModal(); + } + + function expandModal() { + prepareForTransition(dialogDom, () => collapseModal()); + Object.assign(dialogDom.style, { + transform: '', + opacity: '', + visibility: 'visible', + }); + } + + function collapseModal() { + const rect = dialogDom.getBoundingClientRect(); + const collapsedRect = refElement.getBoundingClientRect(); + const originX = (collapsedRect.left + collapsedRect.width / 2) - rect.left; + const originY = (collapsedRect.top + collapsedRect.height / 2) - rect.top; + Object.assign(dialogDom.style, { + transform: `scale(${collapsedRect.width / rect.width}, ${collapsedRect.height / rect.height})`, + transformOrigin: `${originX}px ${originY}px`, + opacity: '0', + }); + } + let close = doClose; + let dialogDom: HTMLElement; const modalDom = cssModalBacker( dom.create((owner) => { - const focus = () => dialog.focus(); + const focus = () => dialogDom.focus(); const ctl = ModalControl.create(owner, doClose, focus); close = () => ctl.close(); - const dialog = cssModalDialog( + dialogDom = cssModalDialog( createFn(ctl, owner), + cssModalDialog.cls('-collapsing', variant === 'collapsing'), dom.on('click', (ev) => ev.stopPropagation()), - options.noEscapeKey ? null : dom.onKeyDown({ Escape: close }), - testId('modal-dialog') + noEscapeKey ? null : dom.onKeyDown({ Escape: close }), + testId('modal-dialog'), ); FocusLayer.create(owner, { - defaultFocusElem: dialog, + defaultFocusElem: dialogDom, allowFocus: (elem) => (elem !== document.body), // Pause mousetrap keyboard shortcuts while the modal is shown. Without this, arrow keys // will navigate in a grid underneath the modal, and Enter may open a cell there. pauseMousetrap: true }); - return dialog; + return dialogDom; }), - options.noClickAway ? null : dom.on('click', () => close()), + noClickAway ? null : dom.on('click', () => close()), ); - document.body.appendChild(modalDom); + if (variant === 'collapsing') { expandModal(); } } export interface ISaveModalOptions { @@ -436,6 +489,11 @@ const cssModalDialog = styled('div', ` &-fixed-wide { width: 600px; } + &-collapsing { + transition-property: opacity, transform; + transition-duration: 0.4s; + transition-timing-function: ease-in-out; + } @media ${mediaSmall} { & { width: unset; @@ -471,6 +529,10 @@ const cssFadeIn = keyframes(` from {background-color: transparent} `); +const cssFadeOut = keyframes(` + from {background-color: ${colors.backdrop}} +`); + const cssModalBacker = styled('div', ` position: fixed; display: flex; @@ -485,6 +547,11 @@ const cssModalBacker = styled('div', ` overflow-y: auto; animation-name: ${cssFadeIn}; animation-duration: 0.4s; + + &-collapsing { + animation-name: ${cssFadeOut}; + background-color: transparent; + } `); const cssSpinner = styled('div', ` diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index 8e771027..7e413bfe 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -64,6 +64,7 @@ export const commonUrls = { efcrConnect: 'https://efc-r.com/connect', efcrHelp: 'https://www.nioxus.info/eFCR-Help', + videoTour: 'https://www.youtube.com/embed/qnr2Pfnxdlc?autoplay=1', }; /**