diff --git a/app/client/components/commandList.ts b/app/client/components/commandList.ts index c34fc16e..532b7cab 100644 --- a/app/client/components/commandList.ts +++ b/app/client/components/commandList.ts @@ -25,7 +25,6 @@ export type CommandName = | 'expandSection' | 'leftPanelOpen' | 'rightPanelOpen' - | 'videoTourToolsOpen' | 'cursorDown' | 'cursorUp' | 'cursorRight' @@ -269,11 +268,6 @@ export const groups: CommendGroupDef[] = [{ keys: [], desc: 'Shortcut to open the right panel', }, - { - name: 'videoTourToolsOpen', - keys: [], - desc: 'Shortcut to open video tour from home left panel', - }, { name: 'activateAssistant', keys: [], diff --git a/app/client/models/AppModel.ts b/app/client/models/AppModel.ts index a246bee3..141bc4bc 100644 --- a/app/client/models/AppModel.ts +++ b/app/client/models/AppModel.ts @@ -392,6 +392,10 @@ export class AppModelImpl extends Disposable implements AppModel { this.behavioralPromptsManager.reset(); }; + G.window.resetOnboarding = () => { + getUserPrefObs(this.userPrefsObs, 'showNewUserQuestions').set(true); + }; + this.autoDispose(subscribe(urlState().state, this.topAppModel.orgs, async (_use, s, orgs) => { this._updateLastVisitedOrgDomain(s, orgs); })); diff --git a/app/client/models/HomeModel.ts b/app/client/models/HomeModel.ts index 56e4a84c..f5275b26 100644 --- a/app/client/models/HomeModel.ts +++ b/app/client/models/HomeModel.ts @@ -7,7 +7,7 @@ import {AppModel, reportError} from 'app/client/models/AppModel'; import {reportMessage, UserError} from 'app/client/models/errors'; import {urlState} from 'app/client/models/gristUrlState'; import {ownerName} from 'app/client/models/WorkspaceInfo'; -import {IHomePage} from 'app/common/gristUrls'; +import {IHomePage, isFeatureEnabled} from 'app/common/gristUrls'; import {isLongerThan} from 'app/common/gutil'; import {SortPref, UserOrgPrefs, ViewPref} from 'app/common/Prefs'; import * as roles from 'app/common/roles'; @@ -59,6 +59,8 @@ export interface HomeModel { shouldShowAddNewTip: Observable; + onboardingTutorial: Observable; + createWorkspace(name: string): Promise; renameWorkspace(id: number, name: string): Promise; deleteWorkspace(id: number, forever: boolean): Promise; @@ -141,6 +143,8 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings public readonly shouldShowAddNewTip = Observable.create(this, !this._app.behavioralPromptsManager.hasSeenPopup('addNew')); + public readonly onboardingTutorial = Observable.create(this, null); + private _userOrgPrefs = Observable.create(this, this._app.currentOrg?.userOrgPrefs); constructor(private _app: AppModel, clientScope: ClientScope) { @@ -176,6 +180,8 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings this.importSources.set(importSources); this._app.refreshOrgUsage().catch(reportError); + + this._loadWelcomeTutorial().catch(reportError); } // Accessor for the AppModel containing this HomeModel. @@ -370,6 +376,28 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings return templateWss; } + private async _loadWelcomeTutorial() { + const {templateOrg, onboardingTutorialDocId} = getGristConfig(); + if ( + !isFeatureEnabled('tutorials') || + !templateOrg || + !onboardingTutorialDocId || + this._app.dismissedPopups.get().includes('onboardingCards') + ) { + return; + } + + try { + const doc = await this._app.api.getTemplate(onboardingTutorialDocId); + if (this.isDisposed()) { return; } + + this.onboardingTutorial.set(doc); + } catch (e) { + console.error(e); + reportError('Failed to load welcome tutorial'); + } + } + private async _saveUserOrgPref(key: K, value: UserOrgPrefs[K]) { const org = this._app.currentOrg; if (org) { diff --git a/app/client/ui/AddNewTip.ts b/app/client/ui/AddNewTip.ts index 43ae62dd..2f34a497 100644 --- a/app/client/ui/AddNewTip.ts +++ b/app/client/ui/AddNewTip.ts @@ -1,13 +1,7 @@ import {HomeModel} from 'app/client/models/HomeModel'; -import {shouldShowWelcomeQuestions} from 'app/client/ui/WelcomeQuestions'; export function attachAddNewTip(home: HomeModel): (el: Element) => void { return () => { - const {app: {userPrefsObs}} = home; - if (shouldShowWelcomeQuestions(userPrefsObs)) { - return; - } - if (shouldShowAddNewTip(home)) { showAddNewTip(home); } diff --git a/app/client/ui/AppUI.ts b/app/client/ui/AppUI.ts index 23ff48d3..5d015ea7 100644 --- a/app/client/ui/AppUI.ts +++ b/app/client/ui/AppUI.ts @@ -13,6 +13,7 @@ import {createDocMenu} from 'app/client/ui/DocMenu'; import {createForbiddenPage, createNotFoundPage, createOtherErrorPage} from 'app/client/ui/errorPages'; import {createHomeLeftPane} from 'app/client/ui/HomeLeftPane'; import {buildSnackbarDom} from 'app/client/ui/NotifyUI'; +import {OnboardingPage, shouldShowOnboardingPage} from 'app/client/ui/OnboardingPage'; import {pagePanels} from 'app/client/ui/PagePanels'; import {RightPanel} from 'app/client/ui/RightPanel'; import {createTopBarDoc, createTopBarHome} from 'app/client/ui/TopBar'; @@ -90,6 +91,10 @@ function createMainPage(appModel: AppModel, appObj: App) { } function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel, app: App) { + if (shouldShowOnboardingPage(appModel.userPrefsObs)) { + return dom.create(OnboardingPage, appModel); + } + const pageModel = HomeModelImpl.create(owner, appModel, app.clientScope); const leftPanelOpen = Observable.create(owner, true); diff --git a/app/client/ui/DocMenu.ts b/app/client/ui/DocMenu.ts index e2e149f0..af5e4905 100644 --- a/app/client/ui/DocMenu.ts +++ b/app/client/ui/DocMenu.ts @@ -13,16 +13,15 @@ import {attachAddNewTip} from 'app/client/ui/AddNewTip'; import * as css from 'app/client/ui/DocMenuCss'; import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro'; import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades'; -import {buildTutorialCard} from 'app/client/ui/TutorialCard'; import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs'; import {shadowScroll} from 'app/client/ui/shadowScroll'; import {makeShareDocUrl} from 'app/client/ui/ShareMenu'; import {transition} from 'app/client/ui/transitions'; import {shouldShowWelcomeCoachingCall, showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall'; -import {shouldShowWelcomeQuestions, showWelcomeQuestions} from 'app/client/ui/WelcomeQuestions'; import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour'; import {buttonSelect, cssButtonSelect} from 'app/client/ui2018/buttonSelect'; import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars'; +import {buildOnboardingCards} from 'app/client/ui/OnboardingCards'; import {icon} from 'app/client/ui2018/icons'; import {loadingSpinner} from 'app/client/ui2018/loaders'; import {menu, menuItem, menuText, select} from 'app/client/ui2018/menus'; @@ -62,10 +61,8 @@ export function createDocMenu(home: HomeModel): DomElementArg[] { function attachWelcomePopups(home: HomeModel): (el: Element) => void { return (element: Element) => { - const {app, app: {userPrefsObs}} = home; - if (shouldShowWelcomeQuestions(userPrefsObs)) { - showWelcomeQuestions(userPrefsObs); - } else if (shouldShowWelcomeCoachingCall(app)) { + const {app} = home; + if (shouldShowWelcomeCoachingCall(app)) { showWelcomeCoachingCall(element, app); } }; @@ -75,116 +72,117 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) { const flashDocId = observable(null); const upgradeButton = buildUpgradeButton(owner, home.app); return css.docList( /* vbox */ - /* first line */ - dom.create(buildTutorialCard, { app: home.app }), - /* hbox */ - css.docListContent( - /* left column - grow 1 */ - css.docMenu( - attachAddNewTip(home), - - dom.maybe(!home.app.currentFeatures?.workspaces, () => [ - css.docListHeader(t("This service is not available right now")), - dom('span', t("(The organization needs a paid plan)")), - ]), - - // currentWS and showIntro observables change together. We capture both in one domComputed call. - dom.domComputed<[IHomePage, Workspace|undefined, boolean]>( - (use) => [use(home.currentPage), use(home.currentWS), use(home.showIntro)], - ([page, workspace, showIntro]) => { - const viewSettings: ViewSettings = - page === 'trash' ? makeLocalViewSettings(home, 'trash') : - page === 'templates' ? makeLocalViewSettings(home, 'templates') : - workspace ? makeLocalViewSettings(home, workspace.id) : - home; - return [ - buildPrefs( - viewSettings, - // Hide the sort and view options when showing the intro. - {hideSort: showIntro, hideView: showIntro && page === 'all'}, - ['all', 'workspace'].includes(page) - ? upgradeButton.showUpgradeButton(css.upgradeButton.cls('')) - : null, - ), - - // Build the pinned docs dom. Builds nothing if the selectedOrg is unloaded. - // TODO: this is shown on all pages, but there is a hack in currentWSPinnedDocs that - // removes all pinned docs when on trash page. - dom.maybe((use) => use(home.currentWSPinnedDocs).length > 0, () => [ - css.docListHeader(css.pinnedDocsIcon('PinBig'), t("Pinned Documents")), - createPinnedDocs(home, home.currentWSPinnedDocs), - ]), - - // Build the featured templates dom if on the Examples & Templates page. - dom.maybe((use) => page === 'templates' && use(home.featuredTemplates).length > 0, () => [ - css.featuredTemplatesHeader( - css.featuredTemplatesIcon('Idea'), - t("Featured"), - testId('featured-templates-header') + /* first line */ + dom.create(buildOnboardingCards, {homeModel: home}), + /* hbox */ + css.docListContent( + /* left column - grow 1 */ + css.docMenu( + attachAddNewTip(home), + + dom.maybe(!home.app.currentFeatures?.workspaces, () => [ + css.docListHeader(t("This service is not available right now")), + dom('span', t("(The organization needs a paid plan)")), + ]), + + // currentWS and showIntro observables change together. We capture both in one domComputed call. + dom.domComputed<[IHomePage, Workspace|undefined, boolean]>( + (use) => [use(home.currentPage), use(home.currentWS), use(home.showIntro)], + ([page, workspace, showIntro]) => { + const viewSettings: ViewSettings = + page === 'trash' ? makeLocalViewSettings(home, 'trash') : + page === 'templates' ? makeLocalViewSettings(home, 'templates') : + workspace ? makeLocalViewSettings(home, workspace.id) : + home; + return [ + buildPrefs( + viewSettings, + // Hide the sort and view options when showing the intro. + {hideSort: showIntro, hideView: showIntro && page === 'all'}, + ['all', 'workspace'].includes(page) + ? upgradeButton.showUpgradeButton(css.upgradeButton.cls('')) + : null, ), - createPinnedDocs(home, home.featuredTemplates, true), - ]), - - dom.maybe(home.available, () => [ - buildOtherSites(home), - (showIntro && page === 'all' ? - null : - css.docListHeader( - ( - page === 'all' ? t("All Documents") : - page === 'templates' ? - dom.domComputed(use => use(home.featuredTemplates).length > 0, (hasFeaturedTemplates) => - hasFeaturedTemplates ? t("More Examples and Templates") : t("Examples and Templates") - ) : - page === 'trash' ? t("Trash") : - workspace && [css.docHeaderIcon('Folder'), workspaceName(home.app, workspace)] - ), - testId('doc-header'), - ) - ), - ( - (page === 'all') ? - dom('div', - showIntro ? buildHomeIntro(home) : null, - buildAllDocsBlock(home, home.workspaces, showIntro, flashDocId, viewSettings), - shouldShowTemplates(home, showIntro) ? buildAllDocsTemplates(home, viewSettings) : null, - ) : - (page === 'trash') ? - dom('div', - css.docBlock(t("Documents stay in Trash for 30 days, after which they get deleted permanently.")), - dom.maybe((use) => use(home.trashWorkspaces).length === 0, () => - css.docBlock(t("Trash is empty.")) + + // Build the pinned docs dom. Builds nothing if the selectedOrg is unloaded. + // TODO: this is shown on all pages, but there is a hack in currentWSPinnedDocs that + // removes all pinned docs when on trash page. + dom.maybe((use) => use(home.currentWSPinnedDocs).length > 0, () => [ + css.docListHeader(css.pinnedDocsIcon('PinBig'), t("Pinned Documents")), + createPinnedDocs(home, home.currentWSPinnedDocs), + ]), + + // Build the featured templates dom if on the Examples & Templates page. + dom.maybe((use) => page === 'templates' && use(home.featuredTemplates).length > 0, () => [ + css.featuredTemplatesHeader( + css.featuredTemplatesIcon('Idea'), + t("Featured"), + testId('featured-templates-header') + ), + createPinnedDocs(home, home.featuredTemplates, true), + ]), + + dom.maybe(home.available, () => [ + buildOtherSites(home), + (showIntro && page === 'all' ? + null : + css.docListHeader( + ( + page === 'all' ? t("All Documents") : + page === 'templates' ? + dom.domComputed(use => use(home.featuredTemplates).length > 0, (hasFeaturedTemplates) => + hasFeaturedTemplates ? t("More Examples and Templates") : t("Examples and Templates") + ) : + page === 'trash' ? t("Trash") : + workspace && [css.docHeaderIcon('Folder'), workspaceName(home.app, workspace)] ), - buildAllDocsBlock(home, home.trashWorkspaces, false, flashDocId, viewSettings), - ) : - (page === 'templates') ? - dom('div', - buildAllTemplates(home, home.templateWorkspaces, viewSettings) - ) : - workspace && !workspace.isSupportWorkspace && workspace.docs?.length ? - css.docBlock( - buildWorkspaceDocBlock(home, workspace, flashDocId, viewSettings), - testId('doc-block') + testId('doc-header'), + ) + ), + ( + (page === 'all') ? + dom('div', + showIntro ? buildHomeIntro(home) : null, + buildAllDocsBlock(home, home.workspaces, showIntro, flashDocId, viewSettings), + shouldShowTemplates(home, showIntro) ? buildAllDocsTemplates(home, viewSettings) : null, ) : - workspace && !workspace.isSupportWorkspace && workspace.docs?.length === 0 ? - buildWorkspaceIntro(home) : - css.docBlock(t("Workspace not found")) - ) - ]), + (page === 'trash') ? + dom('div', + css.docBlock(t("Documents stay in Trash for 30 days, after which they get deleted permanently.")), + dom.maybe((use) => use(home.trashWorkspaces).length === 0, () => + css.docBlock(t("Trash is empty.")) + ), + buildAllDocsBlock(home, home.trashWorkspaces, false, flashDocId, viewSettings), + ) : + (page === 'templates') ? + dom('div', + buildAllTemplates(home, home.templateWorkspaces, viewSettings) + ) : + workspace && !workspace.isSupportWorkspace && workspace.docs?.length ? + css.docBlock( + buildWorkspaceDocBlock(home, workspace, flashDocId, viewSettings), + testId('doc-block') + ) : + workspace && !workspace.isSupportWorkspace && workspace.docs?.length === 0 ? + buildWorkspaceIntro(home) : + css.docBlock(t("Workspace not found")) + ) + ]), + ]; + }), + testId('doclist') + ), + dom.maybe(use => !use(isNarrowScreenObs()) && ['all', 'workspace'].includes(use(home.currentPage)), + () => { + // TODO: These don't currently clash (grist-core stubs the upgradeButton), but a way to + // manage card popups will be needed if more are added later. + return [ + upgradeButton.showUpgradeCard(css.upgradeCard.cls('')), + home.app.supportGristNudge.buildNudgeCard(), ]; }), - testId('doclist') ), - dom.maybe(use => !use(isNarrowScreenObs()) && ['all', 'workspace'].includes(use(home.currentPage)), - () => { - // TODO: These don't currently clash (grist-core stubs the upgradeButton), but a way to - // manage card popups will be needed if more are added later. - return [ - upgradeButton.showUpgradeCard(css.upgradeCard.cls('')), - home.app.supportGristNudge.buildNudgeCard(), - ]; - }), - )); + ); } function buildAllDocsBlock( diff --git a/app/client/ui/DocTutorial.ts b/app/client/ui/DocTutorial.ts index d2c170df..892e1937 100644 --- a/app/client/ui/DocTutorial.ts +++ b/app/client/ui/DocTutorial.ts @@ -1,11 +1,12 @@ import {GristDoc} from 'app/client/components/GristDoc'; +import {makeT} from 'app/client/lib/localization'; import {logTelemetryEvent} from 'app/client/lib/telemetry'; import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState'; import {renderer} from 'app/client/ui/DocTutorialRenderer'; import {cssPopupBody, FLOATING_POPUP_TOOLTIP_KEY, FloatingPopup} from 'app/client/ui/FloatingPopup'; import {sanitizeHTML} from 'app/client/ui/sanitizeHTML'; import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips'; -import {basicButton, primaryButton} from 'app/client/ui2018/buttons'; +import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons'; import {mediaXSmall, theme, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {loadingSpinner} from 'app/client/ui2018/loaders'; @@ -24,6 +25,8 @@ interface DocTutorialSlide { imageUrls: string[]; } +const t = makeT('DocTutorial'); + const testId = makeTestId('test-doc-tutorial-'); export class DocTutorial extends FloatingPopup { @@ -35,12 +38,12 @@ export class DocTutorial extends FloatingPopup { private _docId = this._gristDoc.docId(); private _slides: Observable = Observable.create(this, null); private _currentSlideIndex = Observable.create(this, this._currentFork?.options?.tutorial?.lastSlideIndex ?? 0); + private _percentComplete = this._currentFork?.options?.tutorial?.percentComplete; - - private _saveCurrentSlidePositionDebounced = debounce(this._saveCurrentSlidePosition, 1000, { - // Save new position immediately if at least 1 second has passed since the last change. + private _saveProgressDebounced = debounce(this._saveProgress, 1000, { + // Save progress immediately if at least 1 second has passed since the last change. leading: true, - // Otherwise, wait for the new position to settle for 1 second before saving it. + // Otherwise, wait 1 second before saving. trailing: true }); @@ -49,6 +52,18 @@ export class DocTutorial extends FloatingPopup { minimizable: true, stopClickPropagationOnMove: true, }); + + this.autoDispose(this._currentSlideIndex.addListener((slideIndex) => { + const numSlides = this._slides.get()?.length ?? 0; + if (numSlides > 0) { + this._percentComplete = Math.max( + Math.floor((slideIndex / numSlides) * 100), + this._percentComplete ?? 0 + ); + } else { + this._percentComplete = undefined; + } + })); } public async start() { @@ -103,13 +118,6 @@ export class DocTutorial extends FloatingPopup { const isFirstSlide = slideIndex === 0; const isLastSlide = slideIndex === numSlides - 1; return [ - cssFooterButtonsLeft( - cssPopupFooterButton(icon('Undo'), - hoverTooltip('Restart Tutorial', {key: FLOATING_POPUP_TOOLTIP_KEY}), - dom.on('click', () => this._restartTutorial()), - testId('popup-restart'), - ), - ), cssProgressBar( range(slides.length).map((i) => cssProgressBarDot( hoverTooltip(slides[i].slideTitle, { @@ -121,17 +129,17 @@ export class DocTutorial extends FloatingPopup { testId(`popup-slide-${i + 1}`), )), ), - cssFooterButtonsRight( - basicButton('Previous', + cssFooterButtons( + basicButton(t('Previous'), dom.on('click', async () => { await this._previousSlide(); }), {style: `visibility: ${isFirstSlide ? 'hidden' : 'visible'}`}, testId('popup-previous'), ), - primaryButton(isLastSlide ? 'Finish': 'Next', + primaryButton(isLastSlide ? t('Finish'): t('Next'), isLastSlide - ? dom.on('click', async () => await this._finishTutorial()) + ? dom.on('click', async () => await this._exitTutorial(true)) : dom.on('click', async () => await this._nextSlide()), testId('popup-next'), ), @@ -140,6 +148,21 @@ export class DocTutorial extends FloatingPopup { }), testId('popup-footer'), ), + cssTutorialControls( + cssTextButton( + cssRestartIcon('Undo'), + t('Restart'), + dom.on('click', () => this._restartTutorial()), + testId('popup-restart'), + ), + cssButtonsSeparator(), + cssTextButton( + cssSkipIcon('Skip'), + t('End tutorial'), + dom.on('click', () => this._exitTutorial()), + testId('popup-end-tutorial'), + ), + ), ]; } @@ -161,19 +184,13 @@ export class DocTutorial extends FloatingPopup { } private _logTelemetryEvent(event: 'tutorialOpened' | 'tutorialProgressChanged') { - const currentSlideIndex = this._currentSlideIndex.get(); - const numSlides = this._slides.get()?.length; - let percentComplete: number | undefined = undefined; - if (numSlides !== undefined && numSlides > 0) { - percentComplete = Math.floor(((currentSlideIndex + 1) / numSlides) * 100); - } logTelemetryEvent(event, { full: { tutorialForkIdDigest: this._currentFork?.id, tutorialTrunkIdDigest: this._currentFork?.trunkId, - lastSlideIndex: currentSlideIndex, - numSlides, - percentComplete, + lastSlideIndex: this._currentSlideIndex.get(), + numSlides: this._slides.get()?.length, + percentComplete: this._percentComplete, }, }); } @@ -251,14 +268,13 @@ export class DocTutorial extends FloatingPopup { } } - private async _saveCurrentSlidePosition() { - const currentOptions = this._currentDoc?.options ?? {}; - const currentSlideIndex = this._currentSlideIndex.get(); + private async _saveProgress() { await this._appModel.api.updateDoc(this._docId, { options: { - ...currentOptions, + ...this._currentFork?.options, tutorial: { - lastSlideIndex: currentSlideIndex, + lastSlideIndex: this._currentSlideIndex.get(), + percentComplete: this._percentComplete, } } }); @@ -267,7 +283,7 @@ export class DocTutorial extends FloatingPopup { private async _changeSlide(slideIndex: number) { this._currentSlideIndex.set(slideIndex); - await this._saveCurrentSlidePositionDebounced(); + await this._saveProgressDebounced(); } private async _previousSlide() { @@ -278,9 +294,10 @@ export class DocTutorial extends FloatingPopup { await this._changeSlide(this._currentSlideIndex.get() + 1); } - private async _finishTutorial() { - this._saveCurrentSlidePositionDebounced.cancel(); - await this._saveCurrentSlidePosition(); + private async _exitTutorial(markAsComplete = false) { + this._saveProgressDebounced.cancel(); + if (markAsComplete) { this._percentComplete = 100; } + await this._saveProgressDebounced(); const lastVisitedOrg = this._appModel.lastVisitedOrgDomain.get(); if (lastVisitedOrg) { await urlState().pushUrl({org: lastVisitedOrg}); @@ -298,8 +315,8 @@ export class DocTutorial extends FloatingPopup { }; confirmModal( - 'Do you want to restart the tutorial? All progress will be lost.', - 'Restart', + t('Do you want to restart the tutorial? All progress will be lost.'), + t('Restart'), doRestart, { modalOptions: { @@ -321,7 +338,7 @@ export class DocTutorial extends FloatingPopup { // eslint-disable-next-line no-self-assign img.src = img.src; - setHoverTooltip(img, 'Click to expand', { + setHoverTooltip(img, t('Click to expand'), { key: FLOATING_POPUP_TOOLTIP_KEY, modifiers: { flip: { @@ -357,14 +374,13 @@ export class DocTutorial extends FloatingPopup { } } - const cssPopupFooter = styled('div', ` display: flex; column-gap: 24px; align-items: center; justify-content: space-between; flex-shrink: 0; - padding: 24px 16px 24px 16px; + padding: 16px; border-top: 1px solid ${theme.tutorialsPopupBorder}; `); @@ -375,19 +391,6 @@ const cssTryItOutBox = styled('div', ` background-color: ${theme.tutorialsPopupBoxBg}; `); - - -const cssPopupFooterButton = styled('div', ` - --icon-color: ${theme.controlSecondaryFg}; - padding: 4px; - border-radius: 4px; - cursor: pointer; - - &:hover { - background-color: ${theme.hover}; - } -`); - const cssProgressBar = styled('div', ` display: flex; gap: 8px; @@ -409,11 +412,7 @@ const cssProgressBarDot = styled('div', ` } `); -const cssFooterButtonsLeft = styled('div', ` - flex-shrink: 0; -`); - -const cssFooterButtonsRight = styled('div', ` +const cssFooterButtons = styled('div', ` display: flex; justify-content: flex-end; column-gap: 8px; @@ -473,3 +472,34 @@ const cssSpinner = styled('div', ` align-items: center; height: 100%; `); + +const cssTutorialControls = styled('div', ` + background-color: ${theme.notificationsPanelHeaderBg}; + display: flex; + justify-content: center; + padding: 8px; +`); + +const cssTextButton = styled(textButton, ` + font-weight: 500; + display: flex; + align-items: center; + column-gap: 4px; + padding: 0 16px; +`); + +const cssRestartIcon = styled(icon, ` + width: 14px; + height: 14px; +`); + +const cssButtonsSeparator = styled('div', ` + width: 0; + border-right: 1px solid ${theme.controlFg}; +`); + +const cssSkipIcon = styled(icon, ` + width: 20px; + height: 20px; + margin: 0px -3px; +`); diff --git a/app/client/ui/HomeLeftPane.ts b/app/client/ui/HomeLeftPane.ts index e2bbf02c..895933f7 100644 --- a/app/client/ui/HomeLeftPane.ts +++ b/app/client/ui/HomeLeftPane.ts @@ -31,7 +31,8 @@ export function createHomeLeftPane(leftPanelOpen: Observable, home: Hom const creating = observable(false); const renaming = observable(null); const isAnonymous = !home.app.currentValidUser; - const canCreate = !isAnonymous || getGristConfig().enableAnonPlayground; + const {enableAnonPlayground, templateOrg, onboardingTutorialDocId} = getGristConfig(); + const canCreate = !isAnonymous || enableAnonPlayground; return cssContent( dom.autoDispose(creating), @@ -114,7 +115,7 @@ export function createHomeLeftPane(leftPanelOpen: Observable, home: Hom )), cssTools( cssPageEntry( - dom.show(isFeatureEnabled("templates") && Boolean(getGristConfig().templateOrg)), + dom.show(isFeatureEnabled("templates") && Boolean(templateOrg)), cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "templates"), cssPageLink(cssPageIcon('Board'), cssLinkText(t("Examples & Templates")), urlState().setLinkUrl({homePage: "templates"}), @@ -130,9 +131,9 @@ export function createHomeLeftPane(leftPanelOpen: Observable, home: Hom ), cssSpacer(), cssPageEntry( - dom.show(isFeatureEnabled('tutorials')), + dom.show(isFeatureEnabled('tutorials') && Boolean(templateOrg && onboardingTutorialDocId)), cssPageLink(cssPageIcon('Bookmark'), cssLinkText(t("Tutorial")), - { href: commonUrls.basicTutorial, target: '_blank' }, + urlState().setLinkUrl({org: templateOrg!, doc: onboardingTutorialDocId}), testId('dm-basic-tutorial'), ), ), diff --git a/app/client/ui/OnboardingCards.ts b/app/client/ui/OnboardingCards.ts new file mode 100644 index 00000000..9781eaf7 --- /dev/null +++ b/app/client/ui/OnboardingCards.ts @@ -0,0 +1,232 @@ +import {makeT} from 'app/client/lib/localization'; +import {urlState} from 'app/client/models/gristUrlState'; +import {HomeModel} from 'app/client/models/HomeModel'; +import {openVideoTour} from 'app/client/ui/OpenVideoTour'; +import {bigPrimaryButtonLink} from 'app/client/ui2018/buttons'; +import {colors, theme} from 'app/client/ui2018/cssVars'; +import {icon} from 'app/client/ui2018/icons'; +import {isFeatureEnabled} from 'app/common/gristUrls'; +import {getGristConfig} from 'app/common/urlUtils'; +import {Computed, dom, IDisposableOwner, makeTestId, styled, subscribeElem} from 'grainjs'; + +interface BuildOnboardingCardsOptions { + homeModel: HomeModel; +} + +const t = makeT('OnboardingCards'); + +const testId = makeTestId('test-onboarding-'); + +export function buildOnboardingCards( + owner: IDisposableOwner, + {homeModel}: BuildOnboardingCardsOptions +) { + const {templateOrg, onboardingTutorialDocId} = getGristConfig(); + if (!isFeatureEnabled('tutorials') || !templateOrg || !onboardingTutorialDocId) { return null; } + + const percentComplete = Computed.create(owner, (use) => { + if (!homeModel.app.currentValidUser) { return 0; } + + const tutorial = use(homeModel.onboardingTutorial); + if (!tutorial) { return undefined; } + + return tutorial.forks?.[0]?.options?.tutorial?.percentComplete ?? 0; + }); + + const shouldShowCards = Computed.create(owner, (use) => + !use(homeModel.app.dismissedPopups).includes('onboardingCards')); + + let videoPlayButtonElement: HTMLElement; + + return dom.maybe(shouldShowCards, () => + cssOnboardingCards( + cssTutorialCard( + cssDismissCardsButton( + icon('CrossBig'), + dom.on('click', () => homeModel.app.dismissPopup('onboardingCards', true)), + testId('dismiss-cards'), + ), + cssTutorialCardHeader( + t('Complete our basics tutorial'), + ), + cssTutorialCardSubHeader( + t('Learn the basic of reference columns, linked widgets, column types, & cards.') + ), + cssTutorialCardBody( + cssTutorialProgress( + cssTutorialProgressText( + cssProgressPercentage( + dom.domComputed(percentComplete, (percent) => percent !== undefined ? `${percent}%` : null), + testId('tutorial-percent-complete'), + ), + cssStarIcon('Star'), + ), + cssTutorialProgressBar( + (elem) => subscribeElem(elem, percentComplete, (val) => { + elem.style.setProperty('--percent-complete', String(val ?? 0)); + }) + ), + ), + bigPrimaryButtonLink( + t('Complete the tutorial'), + urlState().setLinkUrl({org: templateOrg, doc: onboardingTutorialDocId}), + ), + ), + testId('tutorial-card'), + ), + cssVideoCard( + cssVideoThumbnail( + cssVideoThumbnailSpacer(), + videoPlayButtonElement = cssVideoPlayButton( + cssPlayIcon('VideoPlay2'), + ), + cssVideoThumbnailText(t('3 minute video tour')), + ), + dom.on('click', () => openVideoTour(videoPlayButtonElement)), + ), + ) + ); +} + +const cssOnboardingCards = styled('div', ` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, max-content)); + gap: 24px; + margin: 24px 0; +`); + +const cssTutorialCard = styled('div', ` + position: relative; + border-radius: 4px; + color: ${theme.announcementPopupFg}; + background-color: ${theme.announcementPopupBg}; + padding: 16px 24px; +`); + +const cssTutorialCardHeader = styled('div', ` + display: flex; + align-items: flex-start; + justify-content: space-between; + font-size: 18px; + font-style: normal; + font-weight: 700; +`); + +const cssDismissCardsButton = styled('div', ` + position: absolute; + top: 8px; + right: 8px; + padding: 4px; + border-radius: 4px; + cursor: pointer; + --icon-color: ${theme.popupCloseButtonFg}; + + &:hover { + background-color: ${theme.hover}; + } +`); + +const cssTutorialCardSubHeader = styled('div', ` + font-size: 14px; + font-style: normal; + font-weight: 500; + margin: 8px 0; +`); + +const cssTutorialCardBody = styled('div', ` + display: flex; + flex-wrap: wrap; + gap: 24px; + margin: 16px 0; + align-items: end; +`); + +const cssTutorialProgress = styled('div', ` + flex: auto; + min-width: 120px; +`); + +const cssTutorialProgressText = styled('div', ` + display: flex; + justify-content: space-between; +`); + +const cssProgressPercentage = styled('div', ` + font-size: 20px; + font-style: normal; + font-weight: 700; +`); + +const cssStarIcon = styled(icon, ` + --icon-color: ${theme.accentIcon}; + width: 24px; + height: 24px; +`); + +const cssTutorialProgressBar = styled('div', ` + margin-top: 4px; + height: 10px; + border-radius: 8px; + background: ${theme.mainPanelBg}; + --percent-complete: 0; + + &::after { + content: ''; + border-radius: 8px; + background: ${theme.progressBarFg}; + display: block; + height: 100%; + width: calc((var(--percent-complete) / 100) * 100%); + } +`); + +const cssVideoCard = styled('div', ` + width: 220px; + height: 158px; + overflow: hidden; + cursor: pointer; + border-radius: 4px; +`); + +const cssVideoThumbnail = styled('div', ` + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 36px 32px; + background-image: url("img/youtube-screenshot.png"); + background-color: rgba(0, 0, 0, 0.4); + background-blend-mode: multiply; + background-size: cover; + transform: scale(1.2); + width: 100%; + height: 100%; +`); + +const cssVideoThumbnailSpacer = styled('div', ``); + +const cssVideoPlayButton = styled('div', ` + display: flex; + justify-content: center; + align-items: center; + align-self: center; + width: 32px; + height: 32px; + background-color: ${theme.controlPrimaryBg}; + border-radius: 50%; + + .${cssVideoThumbnail.className}:hover & { + background-color: ${theme.controlPrimaryHoverBg}; + } +`); + +const cssPlayIcon = styled(icon, ` + --icon-color: ${theme.controlPrimaryFg}; + width: 24px; + height: 24px; +`); + +const cssVideoThumbnailText = styled('div', ` + color: ${colors.light}; + font-weight: 700; + text-align: center; +`); diff --git a/app/client/ui/OnboardingPage.ts b/app/client/ui/OnboardingPage.ts new file mode 100644 index 00000000..ce2b7425 --- /dev/null +++ b/app/client/ui/OnboardingPage.ts @@ -0,0 +1,747 @@ +import {FocusLayer} from 'app/client/lib/FocusLayer'; +import {makeT} from 'app/client/lib/localization'; +import {AppModel} from 'app/client/models/AppModel'; +import {logError} from 'app/client/models/errors'; +import {getMainOrgUrl, urlState} from 'app/client/models/gristUrlState'; +import {getUserPrefObs} from 'app/client/models/UserPrefs'; +import {textInput} from 'app/client/ui/inputs'; +import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer'; +import {bigBasicButton, bigPrimaryButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons'; +import {colors, mediaMedium, mediaXSmall, theme} from 'app/client/ui2018/cssVars'; +import {icon} from 'app/client/ui2018/icons'; +import {IconName} from 'app/client/ui2018/IconList'; +import {modal} from 'app/client/ui2018/modals'; +import {BaseAPI} from 'app/common/BaseAPI'; +import {getPageTitleSuffix, ONBOARDING_VIDEO_YOUTUBE_EMBED_ID} from 'app/common/gristUrls'; +import {UserPrefs} from 'app/common/Prefs'; +import {getGristConfig} from 'app/common/urlUtils'; +import { + Computed, + Disposable, + dom, + DomContents, + IDisposableOwner, + input, + makeTestId, + Observable, + styled, + subscribeElem, +} from 'grainjs'; + +const t = makeT('OnboardingPage'); + +const testId = makeTestId('test-onboarding-'); + +const choices: Array<{icon: IconName, color: string, textKey: string}> = [ + {icon: 'UseProduct', color: `${colors.lightGreen}`, textKey: 'Product Development' }, + {icon: 'UseFinance', color: '#0075A2', textKey: 'Finance & Accounting'}, + {icon: 'UseMedia', color: '#F7B32B', textKey: 'Media Production' }, + {icon: 'UseMonitor', color: '#F2545B', textKey: 'IT & Technology' }, + {icon: 'UseChart', color: '#7141F9', textKey: 'Marketing' }, + {icon: 'UseScience', color: '#231942', textKey: 'Research' }, + {icon: 'UseSales', color: '#885A5A', textKey: 'Sales' }, + {icon: 'UseEducate', color: '#4A5899', textKey: 'Education' }, + {icon: 'UseHr', color: '#688047', textKey: 'HR & Management' }, + {icon: 'UseOther', color: '#929299', textKey: 'Other' }, +]; + +export function shouldShowOnboardingPage(userPrefsObs: Observable): boolean { + return Boolean(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions); +} + +type IncrementStep = (delta?: 1 | -1) => void; + +interface Step { + state?: QuestionsState | VideoState; + buildDom(): DomContents; + onNavigateAway?(): void; +} + +interface QuestionsState { + organization: Observable; + role: Observable; + useCases: Array>; + useOther: Observable; +} + +interface VideoState { + watched: Observable; +} + +export class OnboardingPage extends Disposable { + private _steps: Array; + private _stepIndex: Observable = Observable.create(this, 0); + + constructor(private _appModel: AppModel) { + super(); + + this.autoDispose(this._stepIndex.addListener((_, prevIndex) => { + this._steps[prevIndex].onNavigateAway?.(); + })); + + const incrementStep: IncrementStep = (delta: -1 | 1 = 1) => { + this._stepIndex.set(this._stepIndex.get() + delta); + }; + + this._steps = [ + { + state: { + organization: Observable.create(this, ''), + role: Observable.create(this, ''), + useCases: choices.map(() => Observable.create(this, false)), + useOther: Observable.create(this, ''), + }, + buildDom() { return dom.create(buildQuestions, incrementStep, this.state as QuestionsState); }, + onNavigateAway() { saveQuestions(this.state as QuestionsState); }, + }, + { + state: { + watched: Observable.create(this, false), + }, + buildDom() { return dom.create(buildVideo, incrementStep, this.state as VideoState); }, + }, + { + buildDom() { return dom.create(buildTutorial, incrementStep); }, + }, + ]; + + document.title = `Welcome${getPageTitleSuffix(getGristConfig())}`; + + getUserPrefObs(this._appModel.userPrefsObs, 'showNewUserQuestions').set(undefined); + } + + public buildDom() { + return cssPageContainer( + cssOnboardingPage( + cssSidebar( + cssSidebarContent( + cssSidebarHeading1(t('Welcome')), + cssSidebarHeading2(this._appModel.currentUser!.name + '!'), + testId('sidebar'), + ), + cssGetStarted( + cssGetStartedImg({src: 'img/get-started.png'}), + ), + ), + cssMainPanel( + buildStepper(this._steps, this._stepIndex), + dom.domComputed(this._stepIndex, index => { + return this._steps[index].buildDom(); + }), + ), + testId('page'), + ), + ); + } +} + +function buildStepper(steps: Step[], stepIndex: Observable) { + return cssStepper( + steps.map((_, i) => + cssStep( + cssStepCircle( + cssStepCircle.cls('-done', use => (i < use(stepIndex))), + dom.domComputed(use => i < use(stepIndex), (done) => done ? icon('Tick') : String(i + 1)), + cssStepCircle.cls('-current', use => (i === use(stepIndex))), + dom.on('click', () => { stepIndex.set(i); }), + testId(`step-${i + 1}`) + ) + ) + ) + ); +} + +function saveQuestions(state: QuestionsState) { + const {organization, role, useCases, useOther} = state; + if (!organization.get() && !role.get() && !useCases.map(useCase => useCase.get()).includes(true)) { + return; + } + + const org_name = organization.get(); + const org_role = role.get(); + const use_cases = choices.filter((c, i) => useCases[i].get()).map(c => c.textKey); + const use_other = use_cases.includes('Other') ? useOther.get() : ''; + const submitUrl = new URL(window.location.href); + submitUrl.pathname = '/welcome/info'; + BaseAPI.request(submitUrl.href, { + method: 'POST', + body: JSON.stringify({org_name, org_role, use_cases, use_other}) + }).catch((e) => logError(e)); +} + +function buildQuestions(owner: IDisposableOwner, incrementStep: IncrementStep, state: QuestionsState) { + const {organization, role, useCases, useOther} = state; + const isFilled = Computed.create(owner, (use) => { + return Boolean(use(organization) || use(role) || useCases.map(useCase => use(useCase)).includes(true)); + }); + + return cssQuestions( + cssHeading(t("Tell us who you are")), + cssQuestion( + cssFieldHeading(t('What organization are you with?')), + cssInput( + organization, + {type: 'text', placeholder: t('Your organization')}, + testId('questions-organization'), + ), + ), + cssQuestion( + cssFieldHeading(t('What is your role?')), + cssInput( + role, + {type: 'text', placeholder: t('Your role')}, + testId('questions-role'), + ), + ), + cssQuestion( + cssFieldHeading(t("What brings you to Grist (you can select multiple)?")), + cssUseCases( + choices.map((item, i) => cssUseCase( + cssUseCaseIcon(icon(item.icon)), + cssUseCase.cls('-selected', useCases[i]), + dom.on('click', () => useCases[i].set(!useCases[i].get())), + (item.icon !== 'UseOther' ? + t(item.textKey) : + [ + cssOtherLabel(t(item.textKey)), + cssOtherInput(useOther, {}, {type: 'text', placeholder: t("Type here")}, + // The following subscribes to changes to selection observable, and focuses the input when + // this item is selected. + (elem) => subscribeElem(elem, useCases[i], val => val && setTimeout(() => elem.focus(), 0)), + // It's annoying if clicking into the input toggles selection; better to turn that + // off (user can click icon to deselect). + dom.on('click', ev => ev.stopPropagation()), + // Similarly, ignore Enter/Escape in "Other" textbox, so that they don't submit/close the form. + dom.onKeyDown({ + Enter: (ev, elem) => elem.blur(), + Escape: (ev, elem) => elem.blur(), + }), + ) + ] + ), + testId('questions-use-case'), + )), + ), + ), + cssContinue( + bigPrimaryButton( + t('Next step'), + dom.show(isFilled), + dom.on('click', () => incrementStep()), + testId('next-step'), + ), + bigBasicButton( + t('Skip step'), + dom.hide(isFilled), + dom.on('click', () => incrementStep()), + testId('skip-step'), + ), + ), + testId('questions'), + ); +} + +function buildVideo(_owner: IDisposableOwner, incrementStep: IncrementStep, state: VideoState) { + const {watched} = state; + + function onPlay() { + watched.set(true); + + return modal((ctl, modalOwner) => { + const youtubePlayer = YouTubePlayer.create(modalOwner, + ONBOARDING_VIDEO_YOUTUBE_EMBED_ID, + { + onPlayerReady: (player) => player.playVideo(), + onPlayerStateChange(_player, {data}) { + if (data !== PlayerState.Ended) { return; } + + ctl.close(); + }, + height: '100%', + width: '100%', + origin: getMainOrgUrl(), + }, + cssYouTubePlayer.cls(''), + ); + + return [ + dom.on('click', () => ctl.close()), + elem => { FocusLayer.create(modalOwner, {defaultFocusElem: elem, pauseMousetrap: true}); }, + dom.onKeyDown({ + Escape: () => ctl.close(), + ' ': () => youtubePlayer.playPause(), + }), + cssModalHeader( + cssModalCloseButton( + cssCloseIcon('CrossBig'), + ), + ), + cssModalBody( + cssVideoPlayer( + dom.on('click', (ev) => ev.stopPropagation()), + youtubePlayer.buildDom(), + testId('video-player'), + ), + cssModalButtons( + bigPrimaryButton( + t('Next step'), + dom.on('click', (ev) => { + ev.stopPropagation(); + ctl.close(); + incrementStep(); + }), + ), + ), + ), + cssVideoPlayerModal.cls(''), + ]; + }); + } + + return dom('div', + cssHeading(t('Discover Grist in 3 minutes')), + cssScreenshot( + dom.on('click', onPlay), + dom('div', + cssScreenshotImg({src: 'img/youtube-screenshot.png'}), + cssActionOverlay( + cssAction( + cssRoundButton(cssVideoPlayIcon('VideoPlay')), + ), + ), + ), + testId('video-thumbnail'), + ), + cssContinue( + cssBackButton( + t('Back'), + dom.on('click', () => incrementStep(-1)), + testId('back'), + ), + bigPrimaryButton( + t('Next step'), + dom.show(watched), + dom.on('click', () => incrementStep()), + testId('next-step'), + ), + bigBasicButton( + t('Skip step'), + dom.hide(watched), + dom.on('click', () => incrementStep()), + testId('skip-step'), + ), + ), + testId('video'), + ); +} + +function buildTutorial(_owner: IDisposableOwner, incrementStep: IncrementStep) { + const {templateOrg, onboardingTutorialDocId} = getGristConfig(); + return dom('div', + cssHeading( + t('Go hands-on with the Grist Basics tutorial'), + cssSubHeading( + t("Grist may look like a spreadsheet, but it doesn't always " + + "act like one. Discover what makes Grist different." + ), + ), + ), + cssTutorial( + cssScreenshot( + dom.on('click', () => urlState().pushUrl({org: templateOrg!, doc: onboardingTutorialDocId})), + cssTutorialScreenshotImg({src: 'img/tutorial-screenshot.png'}), + cssTutorialOverlay( + cssAction( + cssTutorialButton(t('Go to the tutorial!')), + ), + ), + testId('tutorial-thumbnail'), + ), + ), + cssContinue( + cssBackButton( + t('Back'), + dom.on('click', () => incrementStep(-1)), + testId('back'), + ), + bigBasicButton( + t('Skip tutorial'), + dom.on('click', () => window.location.href = urlState().makeUrl(urlState().state.get())), + testId('skip-tutorial'), + ), + ), + testId('tutorial'), + ); +} + +const cssPageContainer = styled('div', ` + overflow: auto; + height: 100%; + background-color: ${theme.mainPanelBg}; +`); + +const cssOnboardingPage = styled('div', ` + display: flex; + min-height: 100%; +`); + +const cssSidebar = styled('div', ` + width: 460px; + background-color: ${colors.lightGreen}; + color: ${colors.light}; + background-image: + linear-gradient(to bottom, rgb(41, 185, 131) 32px, transparent 32px), + linear-gradient(to right, rgb(41, 185, 131) 32px, transparent 32px); + background-size: 240px 120px; + background-position: 0 0, 40%; + display: flex; + flex-direction: column; + + @media ${mediaMedium} { + & { + display: none; + } + } +`); + +const cssGetStarted = styled('div', ` + width: 500px; + height: 350px; + margin: auto -77px 0 37px; + overflow: hidden; +`); + +const cssGetStartedImg = styled('img', ` + display: block; + width: 500px; + height: auto; +`); + +const cssSidebarContent = styled('div', ` + line-height: 32px; + margin: 112px 16px 64px 16px; + font-size: 24px; + line-height: 48px; + font-weight: 500; +`); + +const cssSidebarHeading1 = styled('div', ` + font-size: 32px; + text-align: center; +`); + +const cssSidebarHeading2 = styled('div', ` + font-size: 28px; + text-align: center; +`); + +const cssMainPanel = styled('div', ` + margin: 56px auto; + padding: 0px 96px; + text-align: center; + + @media ${mediaMedium} { + & { + padding: 0px 32px; + } + } +`); + +const cssHeading = styled('div', ` + color: ${theme.text}; + font-size: 24px; + font-weight: 500; + margin: 32px 0px; +`); + +const cssSubHeading = styled(cssHeading, ` + font-size: 15px; + font-weight: 400; + margin-top: 16px; +`); + +const cssStep = styled('div', ` + display: flex; + align-items: center; + cursor: default; + + &:not(:last-child)::after { + content: ""; + width: 50px; + height: 2px; + background-color: var(--grist-color-light-green); + } +`); + +const cssStepCircle = styled('div', ` + --icon-color: ${theme.controlPrimaryFg}; + --step-color: ${theme.controlPrimaryBg}; + display: inline-block; + width: 24px; + height: 24px; + border-radius: 30px; + border: 1px solid var(--step-color); + color: var(--step-color); + margin: 4px; + position: relative; + cursor: pointer; + + &:hover { + --step-color: ${theme.controlPrimaryHoverBg}; + } + &-current { + background-color: var(--step-color); + color: ${theme.controlPrimaryFg}; + outline: 3px solid ${theme.cursorInactive}; + } + &-done { + background-color: var(--step-color); + } +`); + +const cssQuestions = styled('div', ` + max-width: 500px; +`); + +const cssQuestion = styled('div', ` + margin: 16px 0 8px 0; + text-align: left; +`); + +const cssFieldHeading = styled('div', ` + color: ${theme.text}; + font-size: 13px; + font-weight: 700; + margin-bottom: 12px; +`); + +const cssContinue = styled('div', ` + display: flex; + justify-content: center; + margin-top: 40px; + gap: 16px; +`); + +const cssUseCases = styled('div', ` + display: flex; + flex-wrap: wrap; + align-items: center; + margin: -8px -4px; +`); + +const cssUseCase = styled('div', ` + flex: 1 0 40%; + min-width: 200px; + margin: 8px 4px 0 4px; + height: 40px; + border: 1px solid ${theme.inputBorder}; + border-radius: 3px; + display: flex; + align-items: center; + text-align: left; + cursor: pointer; + color: ${theme.text}; + --icon-color: ${theme.accentIcon}; + + &:hover { + background-color: ${theme.hover}; + } + &-selected { + border: 2px solid ${theme.controlFg}; + } + &-selected:hover { + border: 2px solid ${theme.controlHoverFg}; + } + &-selected:focus-within { + box-shadow: 0 0 2px 0px ${theme.controlFg}; + } +`); + +const cssUseCaseIcon = styled('div', ` + margin: 0 16px; + --icon-color: ${theme.accentIcon}; +`); + +const cssOtherLabel = styled('div', ` + display: block; + + .${cssUseCase.className}-selected & { + display: none; + } +`); + +const cssInput = styled(textInput, ` + height: 40px; +`); + +const cssOtherInput = styled(input, ` + color: ${theme.inputFg}; + display: none; + border: none; + background: none; + outline: none; + padding: 0px; + + &::placeholder { + color: ${theme.inputPlaceholderFg}; + } + .${cssUseCase.className}-selected & { + display: block; + } +`); + +const cssTutorial = styled('div', ` + display: flex; + justify-content: center; +`); + +const cssScreenshot = styled('div', ` + max-width: 720px; + display: flex; + position: relative; + border-radius: 3px; + border: 3px solid ${colors.lightGreen}; + overflow: hidden; + cursor: pointer; +`); + +const cssActionOverlay = styled('div', ` + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.20); +`); + +const cssTutorialOverlay = styled(cssActionOverlay, ` + background-color: transparent; +`); + +const cssAction = styled('div', ` + display: flex; + flex-direction: column; + margin: auto; + align-items: center; + justify-content: center; + height: 100%; +`); + +const cssVideoPlayIcon = styled(icon, ` + --icon-color: ${colors.light}; + width: 38px; + height: 33.25px; +`); + +const cssCloseIcon = styled(icon, ` + --icon-color: ${colors.light}; + width: 22px; + height: 22px; +`); + +const cssYouTubePlayer = styled('iframe', ` + border-radius: 4px; +`); + +const cssModalHeader = styled('div', ` + display: flex; + flex-shrink: 0; + justify-content: flex-end; +`); + +const cssModalBody = styled('div', ` + display: flex; + flex-grow: 1; + flex-direction: column; + justify-content: center; + align-items: center; +`); + +const cssBackButton = styled(bigBasicButton, ` + border: none; +`); + +const cssModalButtons = styled('div', ` + display: flex; + justify-content: center; + margin-top: 24px; +`); + +const cssVideoPlayer = styled('div', ` + width: 100%; + max-width: 1280px; + height: 100%; + max-height: 720px; + + @media ${mediaXSmall} { + & { + max-height: 240px; + } + } +`); + +const cssVideoPlayerModal = styled('div', ` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: 8px; + background-color: transparent; + box-shadow: none; +`); + +const cssModalCloseButton = styled('div', ` + margin-bottom: 8px; + padding: 4px; + border-radius: 4px; + cursor: pointer; + + &:hover { + background-color: ${theme.hover}; + } +`); + +const cssScreenshotImg = styled('img', ` + transform: scale(1.2); + width: 100%; +`); + +const cssTutorialScreenshotImg = styled('img', ` + width: 100%; + opacity: 0.4; +`); + +const cssRoundButton = styled('div', ` + width: 75px; + height: 75px; + flex-shrink: 0; + border-radius: 100px; + background: ${colors.lightGreen}; + display: flex; + align-items: center; + justify-content: center; + --icon-color: var(--light, #FFF); + + .${cssScreenshot.className}:hover & { + background: ${colors.darkGreen}; + } +`); + +const cssStepper = styled('div', ` + display: flex; + justify-content: center; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 20px; + text-transform: uppercase; +`); + +const cssTutorialButton = styled(bigPrimaryButtonLink, ` + .${cssScreenshot.className}:hover & { + background-color: ${theme.controlPrimaryHoverBg}; + border-color: ${theme.controlPrimaryHoverBg}; + } +`); diff --git a/app/client/ui/OpenVideoTour.ts b/app/client/ui/OpenVideoTour.ts index 00a44394..8bd230d3 100644 --- a/app/client/ui/OpenVideoTour.ts +++ b/app/client/ui/OpenVideoTour.ts @@ -1,4 +1,3 @@ -import * as commands from 'app/client/components/commands'; import {makeT} from 'app/client/lib/localization'; import {logTelemetryEvent} from 'app/client/lib/telemetry'; import {getMainOrgUrl} from 'app/client/models/gristUrlState'; @@ -7,15 +6,13 @@ import {YouTubePlayer} from 'app/client/ui/YouTubePlayer'; import {theme} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {cssModalCloseButton, modal} from 'app/client/ui2018/modals'; -import {isFeatureEnabled} from 'app/common/gristUrls'; -import {dom, makeTestId, styled} from 'grainjs'; +import {isFeatureEnabled, ONBOARDING_VIDEO_YOUTUBE_EMBED_ID} from 'app/common/gristUrls'; +import {dom, keyframes, makeTestId, styled} from 'grainjs'; const t = makeT('OpenVideoTour'); const testId = makeTestId('test-video-tour-'); -const VIDEO_TOUR_YOUTUBE_EMBED_ID = '56AieR9rpww'; - /** * Opens a modal containing a video tour of Grist. */ @@ -23,7 +20,7 @@ const VIDEO_TOUR_YOUTUBE_EMBED_ID = '56AieR9rpww'; return modal( (ctl, owner) => { const youtubePlayer = YouTubePlayer.create(owner, - VIDEO_TOUR_YOUTUBE_EMBED_ID, + ONBOARDING_VIDEO_YOUTUBE_EMBED_ID, { onPlayerReady: (player) => player.playVideo(), height: '100%', @@ -83,12 +80,7 @@ export function createVideoTourToolsButton(): HTMLDivElement | null { let iconElement: HTMLElement; - const commandsGroup = commands.createGroup({ - videoTourToolsOpen: () => openVideoTour(iconElement), - }, null, true); - return cssPageEntryMain( - dom.autoDispose(commandsGroup), cssPageLink( iconElement = cssPageIcon('Video'), cssLinkText(t("Video Tour")), @@ -108,10 +100,19 @@ const cssModal = styled('div', ` max-width: 864px; `); +const delayedVisibility = keyframes(` + to { + visibility: visible; + } +`); + const cssYouTubePlayerContainer = styled('div', ` position: relative; padding-bottom: 56.25%; height: 0; + /* Wait until the modal is finished animating. */ + visibility: hidden; + animation: 0s linear 0.4s forwards ${delayedVisibility}; `); const cssYouTubePlayer = styled('div', ` diff --git a/app/client/ui/TutorialCard.ts b/app/client/ui/TutorialCard.ts deleted file mode 100644 index 538ded32..00000000 --- a/app/client/ui/TutorialCard.ts +++ /dev/null @@ -1,217 +0,0 @@ -import {AppModel} from 'app/client/models/AppModel'; -import {bigPrimaryButton} from 'app/client/ui2018/buttons'; -import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars'; -import {icon} from 'app/client/ui2018/icons'; -import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls'; -import {Computed, dom, IDisposableOwner, makeTestId, styled} from 'grainjs'; - -const testId = makeTestId('test-tutorial-card-'); - -interface Options { - app: AppModel, -} - -export function buildTutorialCard(owner: IDisposableOwner, options: Options) { - if (!isFeatureEnabled('tutorials')) { return null; } - - const {app} = options; - function onClose() { - app.dismissPopup('tutorialFirstCard', true); - } - const visible = Computed.create(owner, (use) => - !use(app.dismissedPopups).includes('tutorialFirstCard') - && !use(isNarrowScreenObs()) - ); - return dom.maybe(visible, () => { - return cssCard( - cssCaption( - dom('div', cssNewToGrist("New to Grist?")), - cssRelative( - cssStartHere("Start here."), - cssArrow() - ), - ), - cssContent( - testId('content'), - cssImage({src: commonUrls.basicTutorialImage}), - cssCardText( - cssLine(cssTitle("Grist Basics Tutorial")), - cssLine("Learn the basics of reference columns, linked widgets, column types, & cards."), - cssLine(cssSub('Beginner - 10 mins')), - cssButtonWrapper( - cssButtonWrapper.cls('-small'), - cssHeroButton("Start Tutorial"), - {href: commonUrls.basicTutorial, target: '_blank'}, - ), - ), - ), - cssButtonWrapper( - cssButtonWrapper.cls('-big'), - cssHeroButton("Start Tutorial"), - {href: commonUrls.basicTutorial, target: '_blank'}, - ), - cssCloseButton(icon('CrossBig'), dom.on('click', () => onClose?.()), testId('close')), - ); - }); -} - -const cssContent = styled('div', ` - position: relative; - display: flex; - align-items: flex-start; - padding-top: 24px; - padding-bottom: 20px; - padding-right: 20px; - max-width: 460px; -`); - -const cssCardText = styled('div', ` - display: flex; - flex-direction: column; - justify-content: center; - align-self: stretch; - margin-left: 12px; -`); - -const cssRelative = styled('div', ` - position: relative; -`); - -const cssNewToGrist = styled('span', ` - font-style: normal; - font-weight: 400; - font-size: 24px; - line-height: 16px; - letter-spacing: 0.2px; - white-space: nowrap; -`); - -const cssStartHere = styled('span', ` - font-style: normal; - font-weight: 700; - font-size: 24px; - line-height: 16px; - letter-spacing: 0.2px; - white-space: nowrap; -`); - -const cssCaption = styled('div', ` - display: flex; - flex-direction: column; - gap: 12px; - margin-left: 32px; - margin-top: 42px; - margin-right: 64px; -`); - -const cssTitle = styled('span', ` - font-weight: 600; - font-size: 20px; -`); - -const cssSub = styled('span', ` - font-size: 12px; - color: ${theme.lightText}; -`); - -const cssLine = styled('div', ` - margin-bottom: 6px; -`); - -const cssHeroButton = styled(bigPrimaryButton, ` -`); - -const cssButtonWrapper = styled('a', ` - flex-grow: 1; - display: flex; - justify-content: flex-end; - margin-right: 60px; - align-items: center; - text-decoration: none; - &:hover { - text-decoration: none; - } - &-big .${cssHeroButton.className} { - padding: 16px 28px; - font-weight: 600; - font-size: 20px; - line-height: 1em; - } -`); - -const cssCloseButton = styled('div', ` - flex-shrink: 0; - align-self: flex-end; - cursor: pointer; - --icon-color: ${theme.controlSecondaryFg}; - margin: 8px 8px 4px 0px; - padding: 2px; - border-radius: 4px; - position: absolute; - top: 0; - right: 0; - &:hover { - background-color: ${theme.lightHover}; - } - &:active { - background-color: ${theme.hover}; - } -`); - -const cssImage = styled('img', ` - width: 187px; - height: 145px; - flex: none; -`); - -const cssArrow = styled('div', ` - position: absolute; - background-image: var(--icon-GreenArrow); - width: 94px; - height: 12px; - top: calc(50% - 6px); - left: calc(100% - 12px); - z-index: 1; -`); - - -const cssCard = styled('div', ` - display: flex; - position: relative; - color: ${theme.text}; - border-radius: 3px; - margin-bottom: 24px; - max-width: 1000px; - box-shadow: 0 2px 18px 0 ${theme.modalInnerShadow}, 0 0 1px 0 ${theme.modalOuterShadow}; - & .${cssButtonWrapper.className}-small { - display: none; - } - @media (max-width: 1320px) { - & .${cssButtonWrapper.className}-small { - flex-direction: column; - display: flex; - margin-top: 14px; - align-self: flex-start; - } - & .${cssButtonWrapper.className}-big { - display: none; - } - } - @media (max-width: 1000px) { - & .${cssArrow.className} { - display: none; - } - & .${cssCaption.className} { - flex-direction: row; - margin-bottom: 24px; - } - & { - flex-direction: column; - } - & .${cssContent.className} { - padding: 12px; - max-width: 100%; - margin-bottom: 28px; - } - } -`); diff --git a/app/client/ui/WelcomeQuestions.ts b/app/client/ui/WelcomeQuestions.ts deleted file mode 100644 index fdd752a3..00000000 --- a/app/client/ui/WelcomeQuestions.ts +++ /dev/null @@ -1,176 +0,0 @@ -import {makeT} from 'app/client/lib/localization'; -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'; -import {IconName} from 'app/client/ui2018/IconList'; -import {ISaveModalOptions, saveModal} from 'app/client/ui2018/modals'; -import {BaseAPI} from 'app/common/BaseAPI'; -import {UserPrefs} from 'app/common/Prefs'; -import {getGristConfig} from 'app/common/urlUtils'; -import {dom, input, Observable, styled, subscribeElem} from 'grainjs'; - -const t = makeT('WelcomeQuestions'); - -export function shouldShowWelcomeQuestions(userPrefsObs: Observable): boolean { - return Boolean(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions); -} - -/** - * Shows a modal with welcome questions if surveying is enabled and the user hasn't - * dismissed the modal before. - */ -export function showWelcomeQuestions(userPrefsObs: Observable) { - saveModal((ctl, owner): ISaveModalOptions => { - const selection = choices.map(c => Observable.create(owner, false)); - const otherText = Observable.create(owner, ''); - const showQuestions = getUserPrefObs(userPrefsObs, 'showNewUserQuestions'); - - async function onConfirm() { - const use_cases = choices.filter((c, i) => selection[i].get()).map(c => c.textKey); - const use_other = use_cases.includes("Other") ? otherText.get() : ''; - - const submitUrl = new URL(window.location.href); - submitUrl.pathname = '/welcome/info'; - return BaseAPI.request(submitUrl.href, - {method: 'POST', body: JSON.stringify({use_cases, use_other})}); - } - - - 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', t("Welcome to Grist!"))], - body: buildInfoForm(selection, otherText), - saveLabel: 'Start using Grist', - saveFunc: onConfirm, - hideCancel: true, - width: 'fixed-wide', - modalArgs: cssModalCentered.cls(''), - }; - }); -} - -const choices: Array<{icon: IconName, color: string, textKey: string}> = [ - {icon: 'UseProduct', color: `${colors.lightGreen}`, textKey: 'Product Development' }, - {icon: 'UseFinance', color: '#0075A2', textKey: 'Finance & Accounting' }, - {icon: 'UseMedia', color: '#F7B32B', textKey: 'Media Production' }, - {icon: 'UseMonitor', color: '#F2545B', textKey: 'IT & Technology' }, - {icon: 'UseChart', color: '#7141F9', textKey: 'Marketing' }, - {icon: 'UseScience', color: '#231942', textKey: 'Research' }, - {icon: 'UseSales', color: '#885A5A', textKey: 'Sales' }, - {icon: 'UseEducate', color: '#4A5899', textKey: 'Education' }, - {icon: 'UseHr', color: '#688047', textKey: 'HR & Management' }, - {icon: 'UseOther', color: '#929299', textKey: 'Other' }, -]; - -function buildInfoForm(selection: Observable[], otherText: Observable) { - return [ - dom('span', t("What brings you to Grist? Please help us serve you better.")), - cssChoices( - choices.map((item, i) => cssChoice( - cssIcon(icon(item.icon), {style: `--icon-color: ${item.color}`}), - cssChoice.cls('-selected', selection[i]), - dom.on('click', () => selection[i].set(!selection[i].get())), - (item.icon !== 'UseOther' ? - t(item.textKey) : - [ - cssOtherLabel(t(item.textKey)), - cssOtherInput(otherText, {}, {type: 'text', placeholder: t("Type here")}, - // The following subscribes to changes to selection observable, and focuses the input when - // this item is selected. - (elem) => subscribeElem(elem, selection[i], val => val && setTimeout(() => elem.focus(), 0)), - // It's annoying if clicking into the input toggles selection; better to turn that - // off (user can click icon to deselect). - dom.on('click', ev => ev.stopPropagation()), - // Similarly, ignore Enter/Escape in "Other" textbox, so that they don't submit/close the form. - dom.onKeyDown({ - Enter: (ev, elem) => elem.blur(), - Escape: (ev, elem) => elem.blur(), - }), - ) - ] - ) - )), - testId('welcome-questions'), - ), - ]; -} - -const cssModalCentered = styled('div', ` - text-align: center; -`); - -const cssLogo = styled('div', ` - display: inline-block; - height: 48px; - width: 48px; - background-image: var(--icon-GristLogo); - background-size: 32px 32px; - background-repeat: no-repeat; - background-position: center; -`); - -const cssChoices = styled('div', ` - display: flex; - flex-wrap: wrap; - align-items: center; - margin-top: 24px; -`); - -const cssChoice = styled('div', ` - flex: 1 0 40%; - min-width: 0px; - margin: 8px 4px 0 4px; - height: 40px; - border: 1px solid ${colors.darkGrey}; - border-radius: 3px; - display: flex; - align-items: center; - text-align: left; - cursor: pointer; - - &:hover { - border-color: ${colors.lightGreen}; - } - &-selected { - background-color: ${colors.mediumGrey}; - } - &-selected:hover { - border-color: ${colors.darkGreen}; - } - &-selected:focus-within { - box-shadow: 0 0 2px 0px var(--grist-color-cursor); - border-color: ${colors.lightGreen}; - } -`); - -const cssIcon = styled('div', ` - margin: 0 16px; -`); - -const cssOtherLabel = styled('div', ` - display: block; - .${cssChoice.className}-selected & { - display: none; - } -`); - -const cssOtherInput = styled(input, ` - display: none; - border: none; - background: none; - outline: none; - padding: 0px; - .${cssChoice.className}-selected & { - display: block; - } -`); diff --git a/app/client/ui/YouTubePlayer.ts b/app/client/ui/YouTubePlayer.ts index 10bba6c5..d7db5649 100644 --- a/app/client/ui/YouTubePlayer.ts +++ b/app/client/ui/YouTubePlayer.ts @@ -11,6 +11,7 @@ export interface Player { unMute(): void; setVolume(volume: number): void; getCurrentTime(): number; + getPlayerState(): PlayerState; } export interface PlayerOptions { @@ -93,6 +94,18 @@ export class YouTubePlayer extends Disposable { this._player.playVideo(); } + public pause() { + this._player.pauseVideo(); + } + + public playPause() { + if (this._player.getPlayerState() === PlayerState.Playing) { + this._player.pauseVideo(); + } else { + this._player.playVideo(); + } + } + public setVolume(volume: number) { this._player.setVolume(volume); } diff --git a/app/client/ui2018/IconList.ts b/app/client/ui2018/IconList.ts index c0fcffbf..d58f39d6 100644 --- a/app/client/ui2018/IconList.ts +++ b/app/client/ui2018/IconList.ts @@ -133,13 +133,17 @@ export type IconName = "ChartArea" | "Separator" | "Settings" | "Share" | + "Skip" | "Sort" | "Sparks" | + "Star" | "Tick" | "TickSolid" | "Undo" | "Validation" | "Video" | + "VideoPlay" | + "VideoPlay2" | "Warning" | "Widget" | "Wrap" | @@ -290,13 +294,17 @@ export const IconList: IconName[] = ["ChartArea", "Separator", "Settings", "Share", + "Skip", "Sort", "Sparks", + "Star", "Tick", "TickSolid", "Undo", "Validation", "Video", + "VideoPlay", + "VideoPlay2", "Warning", "Widget", "Wrap", diff --git a/app/common/Prefs.ts b/app/common/Prefs.ts index 3c6c3786..0ca63d0f 100644 --- a/app/common/Prefs.ts +++ b/app/common/Prefs.ts @@ -107,12 +107,15 @@ export interface BehavioralPromptPrefs { export const DismissedPopup = StringUnion( 'deleteRecords', // confirmation for deleting records keyboard shortcut 'deleteFields', // confirmation for deleting columns keyboard shortcut - 'tutorialFirstCard', // first card of the tutorial 'formulaHelpInfo', // formula help info shown in the popup editor 'formulaAssistantInfo', // formula assistant info shown in the popup editor 'supportGrist', // nudge to opt in to telemetry 'publishForm', // confirmation for publishing a form 'unpublishForm', // confirmation for unpublishing a form + 'onboardingCards', // onboarding cards shown on the doc menu + + /* Deprecated */ + 'tutorialFirstCard', // first card of the tutorial ); export type DismissedPopup = typeof DismissedPopup.type; diff --git a/app/common/UserAPI.ts b/app/common/UserAPI.ts index 9c6e824a..dda2d28b 100644 --- a/app/common/UserAPI.ts +++ b/app/common/UserAPI.ts @@ -144,7 +144,7 @@ export interface DocumentOptions { export interface TutorialMetadata { lastSlideIndex?: number; - numSlides?: number; + percentComplete?: number; } export interface DocumentProperties extends CommonProperties { @@ -368,6 +368,7 @@ export interface UserAPI { getOrgWorkspaces(orgId: number|string, includeSupport?: boolean): Promise; getOrgUsageSummary(orgId: number|string): Promise; getTemplates(onlyFeatured?: boolean): Promise; + getTemplate(docId: string): Promise; getDoc(docId: string): Promise; newOrg(props: Partial): Promise; newWorkspace(props: Partial, orgId: number|string): Promise; @@ -587,6 +588,10 @@ export class UserAPIImpl extends BaseAPI implements UserAPI { return this.requestJson(`${this._url}/api/templates?onlyFeatured=${onlyFeatured ? 1 : 0}`, { method: 'GET' }); } + public async getTemplate(docId: string): Promise { + return this.requestJson(`${this._url}/api/templates/${docId}`, { method: 'GET' }); + } + public async getWidgets(): Promise { return await this.requestJson(`${this._url}/api/widgets`, { method: 'GET' }); } diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index 1f66291e..2f81d215 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -101,8 +101,6 @@ export const commonUrls = { formulas: 'https://support.getgrist.com/formulas', forms: 'https://www.getgrist.com/forms/?utm_source=grist-forms&utm_medium=grist-forms&utm_campaign=forms-footer', - basicTutorial: 'https://templates.getgrist.com/woXtXUBmiN5T/Grist-Basics', - basicTutorialImage: 'https://www.getgrist.com/wp-content/uploads/2021/08/lightweight-crm.png', gristLabsCustomWidgets: 'https://gristlabs.github.io/grist-widget/', gristLabsWidgetRepository: 'https://github.com/gristlabs/grist-widget/releases/download/latest/manifest.json', githubGristCore: 'https://github.com/gristlabs/grist-core', @@ -111,6 +109,8 @@ export const commonUrls = { versionCheck: 'https://api.getgrist.com/api/version', }; +export const ONBOARDING_VIDEO_YOUTUBE_EMBED_ID = '56AieR9rpww'; + /** * Values representable in a URL. The current state is available as urlState().state observable * in client. Updates to this state are expected by functions such as makeUrl() and setLinkUrl(). @@ -811,6 +811,9 @@ export interface GristLoadConfig { // The org containing public templates and tutorials. templateOrg?: string|null; + // The doc id of the tutorial shown during onboarding. + onboardingTutorialDocId?: string; + // Whether to show the "Delete Account" button in the account page. canCloseAccount?: boolean; diff --git a/app/gen-server/ApiServer.ts b/app/gen-server/ApiServer.ts index b927d62f..7d283f2f 100644 --- a/app/gen-server/ApiServer.ts +++ b/app/gen-server/ApiServer.ts @@ -302,6 +302,18 @@ export class ApiServer { return sendReply(req, res, query); })); + // GET /api/templates/:did + // Get information about a template. + this._app.get('/api/templates/:did', expressWrap(async (req, res) => { + const templateOrg = getTemplateOrg(); + if (!templateOrg) { + throw new ApiError('Template org is not configured', 501); + } + + const query = await this._dbManager.getDoc({...getScope(req), org: templateOrg}); + return sendOkReply(req, res, query); + })); + // GET /api/widgets/ // Get all widget definitions from external source. this._app.get('/api/widgets/', expressWrap(async (req, res) => { diff --git a/app/gen-server/entity/Document.ts b/app/gen-server/entity/Document.ts index 44e678ff..cc23265d 100644 --- a/app/gen-server/entity/Document.ts +++ b/app/gen-server/entity/Document.ts @@ -134,12 +134,12 @@ export class Document extends Resource { this.options.tutorial = null; } else { this.options.tutorial = this.options.tutorial || {}; - if (props.options.tutorial.numSlides !== undefined) { - this.options.tutorial.numSlides = props.options.tutorial.numSlides; - } if (props.options.tutorial.lastSlideIndex !== undefined) { this.options.tutorial.lastSlideIndex = props.options.tutorial.lastSlideIndex; } + if (props.options.tutorial.percentComplete !== undefined) { + this.options.tutorial.percentComplete = props.options.tutorial.percentComplete; + } } } // Normalize so that null equates with absence. diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index f7d0a946..1ceef806 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -1124,7 +1124,7 @@ export class DocWorkerApi { const scope = getDocScope(req); const tutorialTrunkId = options.sourceDocId; await this._dbManager.connection.transaction(async (manager) => { - // Fetch the tutorial trunk doc so we can replace the tutorial doc's name. + // Fetch the tutorial trunk so we can replace the tutorial fork's name. const tutorialTrunk = await this._dbManager.getDoc({...scope, urlId: tutorialTrunkId}, manager); await this._dbManager.updateDocument( scope, @@ -1132,9 +1132,8 @@ export class DocWorkerApi { name: tutorialTrunk.name, options: { tutorial: { - ...tutorialTrunk.options?.tutorial, - // For now, the only state we need to reset is the slide position. lastSlideIndex: 0, + percentComplete: 0, }, }, }, diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 8004e4e2..06e9b855 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1055,7 +1055,7 @@ export class FlexServer implements GristServer { // Reset isFirstTimeUser flag. await this._dbManager.updateUser(user.id, {isFirstTimeUser: false}); - // This is a good time to set some other flags, for showing a popup with welcome question(s) + // This is a good time to set some other flags, for showing a page with welcome question(s) // to this new user and recording their sign-up with Google Tag Manager. These flags are also // scoped to the user, but isFirstTimeUser has a dedicated DB field because it predates userPrefs. // Note that the updateOrg() method handles all levels of prefs (for user, user+org, or org). @@ -1586,20 +1586,25 @@ export class FlexServer implements GristServer { this.app.post('/welcome/info', ...middleware, expressWrap(async (req, resp, next) => { const userId = getUserId(req); const user = getUser(req); + const orgName = stringParam(req.body.org_name, 'org_name'); + const orgRole = stringParam(req.body.org_role, 'org_role'); const useCases = stringArrayParam(req.body.use_cases, 'use_cases'); const useOther = stringParam(req.body.use_other, 'use_other'); const row = { UserID: userId, Name: user.name, Email: user.loginEmail, + org_name: orgName, + org_role: orgRole, use_cases: ['L', ...useCases], use_other: useOther, }; - this._recordNewUserInfo(row) - .catch(e => { + try { + await this._recordNewUserInfo(row); + } catch (e) { // If we failed to record, at least log the data, so we could potentially recover it. log.rawWarn(`Failed to record new user info: ${e.message}`, {newUserQuestions: row}); - }); + } const nonOtherUseCases = useCases.filter(useCase => useCase !== 'Other'); for (const useCase of [...nonOtherUseCases, ...(useOther ? [`Other - ${useOther}`] : [])]) { this.getTelemetry().logEvent(req as RequestWithLogin, 'answeredUseCaseQuestion', { diff --git a/app/server/lib/gristSettings.ts b/app/server/lib/gristSettings.ts index 3ffb8197..51a79dfe 100644 --- a/app/server/lib/gristSettings.ts +++ b/app/server/lib/gristSettings.ts @@ -11,3 +11,9 @@ export function getTemplateOrg() { } return org; } + +export function getOnboardingTutorialDocId() { + return appSettings.section('tutorials').flag('onboardingTutorialDocId').readString({ + envVar: 'GRIST_ONBOARDING_TUTORIAL_DOC_ID', + }); +} diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts index 3fce3c38..460137fa 100644 --- a/app/server/lib/sendAppPage.ts +++ b/app/server/lib/sendAppPage.ts @@ -16,7 +16,7 @@ import {SUPPORT_EMAIL} from 'app/gen-server/lib/homedb/HomeDBManager'; import {isAnonymousUser, isSingleUserMode, RequestWithLogin} from 'app/server/lib/Authorizer'; import {RequestWithOrg} from 'app/server/lib/extractOrg'; import {GristServer} from 'app/server/lib/GristServer'; -import {getTemplateOrg} from 'app/server/lib/gristSettings'; +import {getOnboardingTutorialDocId, getTemplateOrg} from 'app/server/lib/gristSettings'; import {getSupportedEngineChoices} from 'app/server/lib/serverUtils'; import {readLoadedLngs, readLoadedNamespaces} from 'app/server/localization'; import * as express from 'express'; @@ -97,6 +97,7 @@ export function makeGristConfig(options: MakeGristConfigOptions): GristLoadConfi telemetry: server?.getTelemetry().getTelemetryConfig(req as RequestWithLogin | undefined), deploymentType: server?.getDeploymentType(), templateOrg: getTemplateOrg(), + onboardingTutorialDocId: getOnboardingTutorialDocId(), canCloseAccount: isAffirmative(process.env.GRIST_ACCOUNT_CLOSE), experimentalPlugins: isAffirmative(process.env.GRIST_EXPERIMENTAL_PLUGINS), notifierEnabled: server?.hasNotifier(), diff --git a/static/icons/icons.css b/static/icons/icons.css index 1a87652f..5159b32d 100644 --- a/static/icons/icons.css +++ b/static/icons/icons.css @@ -32,9 +32,9 @@ --icon-FieldText: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSwxMyBMNy41LDMgTDMsMyBMMyw0LjUgQzMsNC43NzYxNDIzNyAyLjc3NjE0MjM3LDUgMi41LDUgQzIuMjIzODU3NjMsNSAyLDQuNzc2MTQyMzcgMiw0LjUgTDIsMi41IEMyLDIuMjIzODU3NjMgMi4yMjM4NTc2MywyIDIuNSwyIEwxMy41LDIgQzEzLjc3NjE0MjQsMiAxNCwyLjIyMzg1NzYzIDE0LDIuNSBMMTQsNC41IEMxNCw0Ljc3NjE0MjM3IDEzLjc3NjE0MjQsNSAxMy41LDUgQzEzLjIyMzg1NzYsNSAxMyw0Ljc3NjE0MjM3IDEzLDQuNSBMMTMsMyBMOC41LDMgTDguNSwxMyBMMTAuNSwxMyBDMTAuNzc2MTQyNCwxMyAxMSwxMy4yMjM4NTc2IDExLDEzLjUgQzExLDEzLjc3NjE0MjQgMTAuNzc2MTQyNCwxNCAxMC41LDE0IEw1LjUsMTQgQzUuMjIzODU3NjMsMTQgNSwxMy43NzYxNDI0IDUsMTMuNSBDNSwxMy4yMjM4NTc2IDUuMjIzODU3NjMsMTMgNS41LDEzIEw3LjUsMTMgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-FieldTextbox: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIuNSwxMyBDMi4yMjM4NTc2MywxMyAyLDEyLjc3NjE0MjQgMiwxMi41IEMyLDEyLjIyMzg1NzYgMi4yMjM4NTc2MywxMiAyLjUsMTIgTDEzLjUsMTIgQzEzLjc3NjE0MjQsMTIgMTQsMTIuMjIzODU3NiAxNCwxMi41IEMxNCwxMi43NzYxNDI0IDEzLjc3NjE0MjQsMTMgMTMuNSwxMyBMMi41LDEzIFogTTIuNSw0IEMyLjIyMzg1NzYzLDQgMiwzLjc3NjE0MjM3IDIsMy41IEMyLDMuMjIzODU3NjMgMi4yMjM4NTc2MywzIDIuNSwzIEwxMy41LDMgQzEzLjc3NjE0MjQsMyAxNCwzLjIyMzg1NzYzIDE0LDMuNSBDMTQsMy43NzYxNDIzNyAxMy43NzYxNDI0LDQgMTMuNSw0IEwyLjUsNCBaIE0yLjUsOC41IEMyLjIyMzg1NzYzLDguNSAyLDguMjc2MTQyMzcgMiw4IEMyLDcuNzIzODU3NjMgMi4yMjM4NTc2Myw3LjUgMi41LDcuNSBMMTMuNSw3LjUgQzEzLjc3NjE0MjQsNy41IDE0LDcuNzIzODU3NjMgMTQsOCBDMTQsOC4yNzYxNDIzNyAxMy43NzYxNDI0LDguNSAxMy41LDguNSBMMi41LDguNSBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4='); --icon-FieldToggle: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTguNjQ1ODI0NCwxMSBMMTAsMTEgQzExLjY1Njg1NDIsMTEgMTMsOS42NTY4NTQyNSAxMyw4IEMxMyw2LjM0MzE0NTc1IDExLjY1Njg1NDIsNSAxMCw1IEw4LjY0NTgyNDQsNSBDOS40NzYyNDUxMSw1LjczMjk0NDQ1IDEwLDYuODA1MzA3NDcgMTAsOCBDMTAsOS4xOTQ2OTI1MyA5LjQ3NjI0NTExLDEwLjI2NzA1NTUgOC42NDU4MjQ0LDExIFogTTYsNCBMMTAsNCBDMTIuMjA5MTM5LDQgMTQsNS43OTA4NjEgMTQsOCBDMTQsMTAuMjA5MTM5IDEyLjIwOTEzOSwxMiAxMCwxMiBMNiwxMiBDMy43OTA4NjEsMTIgMiwxMC4yMDkxMzkgMiw4IEMyLDUuNzkwODYxIDMuNzkwODYxLDQgNiw0IFogTTYsMTEgQzcuNjU2ODU0MjUsMTEgOSw5LjY1Njg1NDI1IDksOCBDOSw2LjM0MzE0NTc1IDcuNjU2ODU0MjUsNSA2LDUgQzQuMzQzMTQ1NzUsNSAzLDYuMzQzMTQ1NzUgMyw4IEMzLDkuNjU2ODU0MjUgNC4zNDMxNDU3NSwxMSA2LDExIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg=='); - --icon-LoginStreamline: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOS41NDAyIDBIMTguMDA0N0M4LjA2NzUyIDAgMCA4Ljk3NzQgMCAyMC4wMzUzVjQwSDIxLjcwNTFDMzEuNTQ4NyA0MCAzOS41NDAyIDMxLjEwNjggMzkuNTQwMiAyMC4xNTMxVjBaIiBmaWxsPSIjRjlBRTQxIiBvcGFjaXR5PSIuMSIvPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjIuNDkxNyAyNy45MzA2TDI4LjQ1MyAxOC44MzA3QzI4LjY1MiAxOC41MjY4IDI4LjU3NzYgMTguMTExOSAyOC4yODY2IDE3LjkwNEMyOC4xODA1IDE3LjgyODEgMjguMDU0OCAxNy43ODc1IDI3LjkyNjIgMTcuNzg3NUgyMy4yNzlWMTEuOTk0OEMyMy4yNzkgMTEuNjI2NiAyMi45OTMyIDExLjMyODEgMjIuNjQwNyAxMS4zMjgxQzIyLjQzIDExLjMyODEgMjIuMjMyOSAxMS40MzY3IDIyLjExMzkgMTEuNjE4M0wxNi4xNTI2IDIwLjcxODNDMTUuOTUzNSAyMS4wMjIxIDE2LjAyOCAyMS40MzcgMTYuMzE4OSAyMS42NDQ5QzE2LjQyNTEgMjEuNzIwOCAxNi41NTA3IDIxLjc2MTQgMTYuNjc5NCAyMS43NjE0SDIxLjMyNjZWMjcuNTU0MkMyMS4zMjY2IDI3LjkyMjMgMjEuNjEyMyAyOC4yMjA4IDIxLjk2NDkgMjguMjIwOEMyMi4xNzU2IDI4LjIyMDggMjIuMzcyNyAyOC4xMTIyIDIyLjQ5MTcgMjcuOTMwNloiIGZpbGw9IiNGOUFFNDEiLz48cGF0aCBvcGFjaXR5PSIuNDQiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTYuMTQ1MyAyNC43NDE5QzE2Ljg1MDMgMjQuNzQxOSAxNy40MjE5IDI1LjMzODggMTcuNDIxOSAyNi4wNzUyVjI2LjM4ODlDMTcuNDIxOSAyNy4xMjUzIDE2Ljg1MDMgMjcuNzIyMyAxNi4xNDUzIDI3LjcyMjNIMTAuODg4N0MxMC4xODM3IDI3LjcyMjMgOS42MTIxMSAyNy4xMjUzIDkuNjEyMTEgMjYuMzg4OVYyNi4wNzUyQzkuNjEyMTEgMjUuMzM4OCAxMC4xODM3IDI0Ljc0MTkgMTAuODg4NyAyNC43NDE5SDE2LjE0NTNaTTEzLjIxNjYgMTguNzgxMUMxMy45MjE3IDE4Ljc4MTEgMTQuNDkzMiAxOS4zNzgxIDE0LjQ5MzIgMjAuMTE0NFYyMC40MjgyQzE0LjQ5MzIgMjEuMTY0NSAxMy45MjE3IDIxLjc2MTUgMTMuMjE2NiAyMS43NjE1SDguOTM2MjZDOC4yMzEyMiAyMS43NjE1IDcuNjU5NjcgMjEuMTY0NSA3LjY1OTY3IDIwLjQyODJWMjAuMTE0NEM3LjY1OTY3IDE5LjM3ODEgOC4yMzEyMiAxOC43ODExIDguOTM2MjYgMTguNzgxMUgxMy4yMTY2Wk0xNi4xNDUzIDEyLjgyMDNDMTYuODUwMyAxMi44MjAzIDE3LjQyMTkgMTMuNDE3MyAxNy40MjE5IDE0LjE1MzZWMTQuNDY3NEMxNy40MjE5IDE1LjIwMzggMTYuODUwMyAxNS44MDA3IDE2LjE0NTMgMTUuODAwN0gxMC44ODg3QzEwLjE4MzcgMTUuODAwNyA5LjYxMjExIDE1LjIwMzggOS42MTIxMSAxNC40Njc0VjE0LjE1MzZDOS42MTIxMSAxMy40MTczIDEwLjE4MzcgMTIuODIwMyAxMC44ODg3IDEyLjgyMDNIMTYuMTQ1M1oiIGZpbGw9IiNGOUFFNDEiLz48L3N2Zz4='); - --icon-LoginUnify: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggb3BhY2l0eT0iLjEiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMzkuNTQwMiAwSDE4LjAwNDdDOC4wNjc1MiAwIDAgOC45Nzc0IDAgMjAuMDM1M1Y0MEgyMS43MDUxQzMxLjU0ODcgNDAgMzkuNTQwMiAzMS4xMDY4IDM5LjU0MDIgMjAuMTUzMVYwWiIgZmlsbD0iIzcxNDFGOSIvPjxwYXRoIG9wYWNpdHk9Ii40NCIgZD0iTTEzLjgyOTcgMTYuNzc4NlYyMy4yMjNDMTMuODI5NyAyNS4wMDI2IDE1LjIxMSAyNi40NDUyIDE2LjkxNDggMjYuNDQ1MkgyMy4wODUxVjI3LjMyNEMyMy4wODUxIDI4Ljg0MzEgMjIuMjk1OCAyOS42Njc1IDIwLjg0MTMgMjkuNjY3NUgxMi45ODgzQzExLjUzMzkgMjkuNjY3NSAxMC43NDQ2IDI4Ljg0MzEgMTAuNzQ0NiAyNy4zMjRWMTkuMTIyQzEwLjc0NDYgMTcuNjAyOSAxMS41MzM5IDE2Ljc3ODYgMTIuOTg4MyAxNi43Nzg2SDEzLjgyOTdaTTI1Ljk4MzIgMTEuNDA4MkMyNy40Mzc2IDExLjQwODIgMjguMjI2OSAxMi4yMzI2IDI4LjIyNjkgMTMuNzUxNlYyMS45NTM3QzI4LjIyNjkgMjMuNDcyNyAyNy40Mzc2IDI0LjI5NzEgMjUuOTgzMiAyNC4yOTcxSDI1LjE0MThWMTcuODUyNkMyNS4xNDE4IDE2LjA3MzEgMjMuNzYwNSAxNC42MzA0IDIyLjA1NjcgMTQuNjMwNEgxNS44ODY1VjEzLjc1MTZDMTUuODg2NSAxMi4yMzI2IDE2LjY3NTggMTEuNDA4MiAxOC4xMzAyIDExLjQwODJIMjUuOTgzMloiIGZpbGw9IiM3MTQxRjkiLz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE3LjE5NTYgMTYuNzc5M0gyMS43NzY1QzIyLjYyNDkgMTYuNzc5MyAyMy4wODUzIDE3LjI2MDIgMjMuMDg1MyAxOC4xNDYzVjIyLjkzMDhDMjMuMDg1MyAyMy44MTY5IDIyLjYyNDkgMjQuMjk3OCAyMS43NzY1IDI0LjI5NzhIMTcuMTk1NkMxNi4zNDcxIDI0LjI5NzggMTUuODg2NyAyMy44MTY5IDE1Ljg4NjcgMjIuOTMwOFYxOC4xNDYzQzE1Ljg4NjcgMTcuMjYwMiAxNi4zNDcxIDE2Ljc3OTMgMTcuMTk1NiAxNi43NzkzWiIgZmlsbD0iIzcxNDFGOSIvPjwvc3ZnPg=='); - --icon-LoginVisualize: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggb3BhY2l0eT0iLjEiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMzkuNTQwMiAwSDE4LjAwNDdDOC4wNjc1MiAwIDAgOC45Nzc0IDAgMjAuMDM1M1Y0MEgyMS43MDUxQzMxLjU0ODcgNDAgMzkuNTQwMiAzMS4xMDY4IDM5LjU0MDIgMjAuMTUzMVYwWiIgZmlsbD0iIzE2QjM3OCIvPjxwYXRoIG9wYWNpdHk9Ii40NCIgZD0iTTIzLjk3MTggMTIuMjIxNkMyMy45NzE4IDExLjM2MjUgMjMuMzA1IDEwLjY2NiAyMi40ODI0IDEwLjY2NkMyMS42NTk5IDEwLjY2NiAyMC45OTMgMTEuMzYyNSAyMC45OTMgMTIuMjIxNlYyNi43NDAxQzIwLjk5MyAyNy41OTkyIDIxLjY1OTkgMjguMjk1NiAyMi40ODI0IDI4LjI5NTZDMjMuMzA1IDI4LjI5NTYgMjMuOTcxOCAyNy41OTkyIDIzLjk3MTggMjYuNzQwMVYxMi4yMjE2WiIgZmlsbD0iIzE2QjM3OCIvPjxwYXRoIGQ9Ik0xOS4wMDcgMTcuNzMwNEMxOS4wMDcgMTYuODcxMyAxOC4zNDAyIDE2LjE3NDggMTcuNTE3NyAxNi4xNzQ4IDE2LjY5NTEgMTYuMTc0OCAxNi4wMjgzIDE2Ljg3MTMgMTYuMDI4MyAxNy43MzA0VjI2LjczOTZDMTYuMDI4MyAyNy41OTg3IDE2LjY5NTEgMjguMjk1MiAxNy41MTc3IDI4LjI5NTIgMTguMzQwMiAyOC4yOTUyIDE5LjAwNyAyNy41OTg3IDE5LjAwNyAyNi43Mzk2VjE3LjczMDR6TTI4LjkzNjIgMTkuOTM0NUMyOC45MzYyIDE5LjA3NTQgMjguMjY5NCAxOC4zNzg5IDI3LjQ0NjkgMTguMzc4OSAyNi42MjQzIDE4LjM3ODkgMjUuOTU3NSAxOS4wNzU0IDI1Ljk1NzUgMTkuOTM0NVYyNi43NEMyNS45NTc1IDI3LjU5OTEgMjYuNjI0MyAyOC4yOTU2IDI3LjQ0NjkgMjguMjk1NiAyOC4yNjk0IDI4LjI5NTYgMjguOTM2MiAyNy41OTkxIDI4LjkzNjIgMjYuNzRWMTkuOTM0NXpNMTQuMDQyNyAyMi4xMzg2QzE0LjA0MjcgMjEuMjc5NSAxMy4zNzU5IDIwLjU4MyAxMi41NTMzIDIwLjU4MyAxMS43MzA4IDIwLjU4MyAxMS4wNjQgMjEuMjc5NSAxMS4wNjQgMjIuMTM4NlYyNi43NDA0QzExLjA2NCAyNy41OTk1IDExLjczMDggMjguMjk2IDEyLjU1MzMgMjguMjk2IDEzLjM3NTkgMjguMjk2IDE0LjA0MjcgMjcuNTk5NSAxNC4wNDI3IDI2Ljc0MDRWMjIuMTM4NnoiIGZpbGw9IiMxNkIzNzgiLz48L3N2Zz4='); + --icon-LoginStreamline: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEzNDlfNjg4NSkiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMzkuNTQwMiAwSDE4LjAwNDdDOC4wNjc1MiAwIDAgOC45Nzc0IDAgMjAuMDM1M1Y0MEgyMS43MDUxQzMxLjU0ODcgNDAgMzkuNTQwMiAzMS4xMDY4IDM5LjU0MDIgMjAuMTUzMVYwWiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0yMy40OTE3IDI3LjkzMDZMMjkuNDUzIDE4LjgzMDdDMjkuNjUyIDE4LjUyNjggMjkuNTc3NiAxOC4xMTE5IDI5LjI4NjYgMTcuOTA0QzI5LjE4MDUgMTcuODI4MSAyOS4wNTQ4IDE3Ljc4NzUgMjguOTI2MiAxNy43ODc1SDI0LjI3OVYxMS45OTQ4QzI0LjI3OSAxMS42MjY2IDIzLjk5MzIgMTEuMzI4MSAyMy42NDA3IDExLjMyODFDMjMuNDMgMTEuMzI4MSAyMy4yMzI5IDExLjQzNjcgMjMuMTEzOSAxMS42MTgzTDE3LjE1MjYgMjAuNzE4M0MxNi45NTM1IDIxLjAyMjEgMTcuMDI4IDIxLjQzNyAxNy4zMTg5IDIxLjY0NDlDMTcuNDI1MSAyMS43MjA4IDE3LjU1MDcgMjEuNzYxNCAxNy42Nzk0IDIxLjc2MTRIMjIuMzI2NlYyNy41NTQyQzIyLjMyNjYgMjcuOTIyMyAyMi42MTIzIDI4LjIyMDggMjIuOTY0OSAyOC4yMjA4QzIzLjE3NTYgMjguMjIwOCAyMy4zNzI3IDI4LjExMjIgMjMuNDkxNyAyNy45MzA2WiIgZmlsbD0iI0Y5QUU0MSIvPjxwYXRoIG9wYWNpdHk9Ii40NCIgZD0iTTE3LjE0NTggMjQuNzQxOUMxNy44NTA4IDI0Ljc0MTkgMTguNDIyNCAyNS4zMzg4IDE4LjQyMjQgMjYuMDc1MlYyNi4zODg5QzE4LjQyMjQgMjcuMTI1MyAxNy44NTA4IDI3LjcyMjMgMTcuMTQ1OCAyNy43MjIzSDExLjg4OTJDMTEuMTg0MSAyNy43MjIzIDEwLjYxMjYgMjcuMTI1MyAxMC42MTI2IDI2LjM4ODlWMjYuMDc1MkMxMC42MTI2IDI1LjMzODggMTEuMTg0MSAyNC43NDE5IDExLjg4OTIgMjQuNzQxOUgxNy4xNDU4Wk0xNC4yMTcxIDE4Ljc4MTFDMTQuOTIyMSAxOC43ODExIDE1LjQ5MzcgMTkuMzc4MSAxNS40OTM3IDIwLjExNDRWMjAuNDI4MkMxNS40OTM3IDIxLjE2NDUgMTQuOTIyMSAyMS43NjE1IDE0LjIxNzEgMjEuNzYxNUg5LjkzNjc1QzkuMjMxNzEgMjEuNzYxNSA4LjY2MDE2IDIxLjE2NDUgOC42NjAxNiAyMC40MjgyVjIwLjExNDRDOC42NjAxNiAxOS4zNzgxIDkuMjMxNzEgMTguNzgxMSA5LjkzNjc1IDE4Ljc4MTFIMTQuMjE3MVpNMTcuMTQ1OCAxMi44MjAzQzE3Ljg1MDggMTIuODIwMyAxOC40MjI0IDEzLjQxNzMgMTguNDIyNCAxNC4xNTM2VjE0LjQ2NzRDMTguNDIyNCAxNS4yMDM4IDE3Ljg1MDggMTUuODAwNyAxNy4xNDU4IDE1LjgwMDdIMTEuODg5MkMxMS4xODQxIDE1LjgwMDcgMTAuNjEyNiAxNS4yMDM4IDEwLjYxMjYgMTQuNDY3NFYxNC4xNTM2QzEwLjYxMjYgMTMuNDE3MyAxMS4xODQxIDEyLjgyMDMgMTEuODg5MiAxMi44MjAzSDE3LjE0NThaIiBmaWxsPSIjRjlBRTQxIi8+PC9nPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDBfMTM0OV82ODg1Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMCAwSDQwVjQwSDB6Ii8+PC9jbGlwUGF0aD48L2RlZnM+PC9zdmc+'); + --icon-LoginUnify: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEzNDlfNjg3MSkiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMzkuNTQwMiAwSDE4LjAwNDdDOC4wNjc1MiAwIDAgOC45Nzc0IDAgMjAuMDM1M1Y0MEgyMS43MDUxQzMxLjU0ODcgNDAgMzkuNTQwMiAzMS4xMDY4IDM5LjU0MDIgMjAuMTUzMVYwWiIgZmlsbD0iI2ZmZiIvPjxwYXRoIG9wYWNpdHk9Ii40NCIgZD0iTTEzLjgzMTIgMTYuNzc4NlYyMy4yMjNDMTMuODMxMiAyNS4wMDI2IDE1LjIxMjUgMjYuNDQ1MiAxNi45MTYzIDI2LjQ0NTJIMjMuMDg2NVYyNy4zMjRDMjMuMDg2NSAyOC44NDMxIDIyLjI5NzIgMjkuNjY3NSAyMC44NDI4IDI5LjY2NzVIMTIuOTg5OEMxMS41MzU0IDI5LjY2NzUgMTAuNzQ2MSAyOC44NDMxIDEwLjc0NjEgMjcuMzI0VjE5LjEyMkMxMC43NDYxIDE3LjYwMjkgMTEuNTM1NCAxNi43Nzg2IDEyLjk4OTggMTYuNzc4NkgxMy44MzEyWk0yNS45ODQ3IDExLjQwODJDMjcuNDM5MSAxMS40MDgyIDI4LjIyODQgMTIuMjMyNiAyOC4yMjg0IDEzLjc1MTZWMjEuOTUzN0MyOC4yMjg0IDIzLjQ3MjcgMjcuNDM5MSAyNC4yOTcxIDI1Ljk4NDcgMjQuMjk3MUgyNS4xNDMzVjE3Ljg1MjZDMjUuMTQzMyAxNi4wNzMxIDIzLjc2MiAxNC42MzA0IDIyLjA1ODIgMTQuNjMwNEgxNS44ODc5VjEzLjc1MTZDMTUuODg3OSAxMi4yMzI2IDE2LjY3NzIgMTEuNDA4MiAxOC4xMzE3IDExLjQwODJIMjUuOTg0N1oiIGZpbGw9IiM3MTQxRjkiLz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE3LjE5NzUgMTYuNzc5M0gyMS43Nzg0QzIyLjYyNjggMTYuNzc5MyAyMy4wODczIDE3LjI2MDIgMjMuMDg3MyAxOC4xNDYzVjIyLjkzMDhDMjMuMDg3MyAyMy44MTY5IDIyLjYyNjggMjQuMjk3OCAyMS43Nzg0IDI0LjI5NzhIMTcuMTk3NUMxNi4zNDkxIDI0LjI5NzggMTUuODg4NyAyMy44MTY5IDE1Ljg4ODcgMjIuOTMwOFYxOC4xNDYzQzE1Ljg4ODcgMTcuMjYwMiAxNi4zNDkxIDE2Ljc3OTMgMTcuMTk3NSAxNi43NzkzWiIgZmlsbD0iIzcxNDFGOSIvPjwvZz48ZGVmcz48Y2xpcFBhdGggaWQ9ImNsaXAwXzEzNDlfNjg3MSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTAgMEg0MFY0MEgweiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg=='); + --icon-LoginVisualize: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEzNDlfNjg5OCkiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMzkuNTQwMiAwSDE4LjAwNDdDOC4wNjc1MiAwIDAgOC45Nzc0IDAgMjAuMDM1M1Y0MEgyMS43MDUxQzMxLjU0ODcgNDAgMzkuNTQwMiAzMS4xMDY4IDM5LjU0MDIgMjAuMTUzMVYwWiIgZmlsbD0iI2ZmZiIvPjxwYXRoIG9wYWNpdHk9Ii40NCIgZD0iTTIzLjk3MjkgMTIuOTk5NkMyMy45NzI5IDEyLjIyNjQgMjMuMzA2MSAxMS41OTk2IDIyLjQ4MzUgMTEuNTk5NkMyMS42NjEgMTEuNTk5NiAyMC45OTQxIDEyLjIyNjQgMjAuOTk0MSAxMi45OTk2VjI2LjA2NjNDMjAuOTk0MSAyNi44Mzk1IDIxLjY2MSAyNy40NjYzIDIyLjQ4MzUgMjcuNDY2M0MyMy4zMDYxIDI3LjQ2NjMgMjMuOTcyOSAyNi44Mzk1IDIzLjk3MjkgMjYuMDY2M1YxMi45OTk2WiIgZmlsbD0iIzE2QjM3OCIvPjxwYXRoIGQ9Ik0xOS4wMDggMTcuOTU3NkMxOS4wMDggMTcuMTg0NCAxOC4zNDEyIDE2LjU1NzYgMTcuNTE4NyAxNi41NTc2IDE2LjY5NjEgMTYuNTU3NiAxNi4wMjkzIDE3LjE4NDQgMTYuMDI5MyAxNy45NTc2VjI2LjA2NkMxNi4wMjkzIDI2LjgzOTEgMTYuNjk2MSAyNy40NjYgMTcuNTE4NyAyNy40NjYgMTguMzQxMiAyNy40NjYgMTkuMDA4IDI2LjgzOTEgMTkuMDA4IDI2LjA2NlYxNy45NTc2ek0yOC45MzU4IDE5Ljk0MUMyOC45MzU4IDE5LjE2NzggMjguMjY4OSAxOC41NDEgMjcuNDQ2NCAxOC41NDEgMjYuNjIzOCAxOC41NDEgMjUuOTU3IDE5LjE2NzggMjUuOTU3IDE5Ljk0MVYyNi4wNjZDMjUuOTU3IDI2LjgzOTIgMjYuNjIzOCAyNy40NjYgMjcuNDQ2NCAyNy40NjYgMjguMjY4OSAyNy40NjYgMjguOTM1OCAyNi44MzkyIDI4LjkzNTggMjYuMDY2VjE5Ljk0MXpNMTQuMDQzMiAyMS45MjU0QzE0LjA0MzIgMjEuMTUyMiAxMy4zNzY0IDIwLjUyNTQgMTIuNTUzOCAyMC41MjU0IDExLjczMTMgMjAuNTI1NCAxMS4wNjQ1IDIxLjE1MjIgMTEuMDY0NSAyMS45MjU0VjI2LjA2NzFDMTEuMDY0NSAyNi44NDAzIDExLjczMTMgMjcuNDY3MSAxMi41NTM4IDI3LjQ2NzEgMTMuMzc2NCAyNy40NjcxIDE0LjA0MzIgMjYuODQwMyAxNC4wNDMyIDI2LjA2NzFWMjEuOTI1NHoiIGZpbGw9IiMxNkIzNzgiLz48L2c+PGRlZnM+PGNsaXBQYXRoIGlkPSJjbGlwMF8xMzQ5XzY4OTgiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wIDBINDBWNDBIMHoiLz48L2NsaXBQYXRoPjwvZGVmcz48L3N2Zz4='); --icon-GoogleLogo: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iIzQyODVGNCIgZD0iTSAtMy4yNjQgNTEuNTA5IEMgLTMuMjY0IDUwLjcxOSAtMy4zMzQgNDkuOTY5IC0zLjQ1NCA0OS4yMzkgTCAtMTQuNzU0IDQ5LjIzOSBMIC0xNC43NTQgNTMuNzQ5IEwgLTguMjg0IDUzLjc0OSBDIC04LjU3NCA1NS4yMjkgLTkuNDI0IDU2LjQ3OSAtMTAuNjg0IDU3LjMyOSBMIC0xMC42ODQgNjAuMzI5IEwgLTYuODI0IDYwLjMyOSBDIC00LjU2NCA1OC4yMzkgLTMuMjY0IDU1LjE1OSAtMy4yNjQgNTEuNTA5IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3LjAwOSAtMzkuMjM5KSIvPjxwYXRoIGZpbGw9IiMzNEE4NTMiIGQ9Ik0gLTE0Ljc1NCA2My4yMzkgQyAtMTEuNTE0IDYzLjIzOSAtOC44MDQgNjIuMTU5IC02LjgyNCA2MC4zMjkgTCAtMTAuNjg0IDU3LjMyOSBDIC0xMS43NjQgNTguMDQ5IC0xMy4xMzQgNTguNDg5IC0xNC43NTQgNTguNDg5IEMgLTE3Ljg4NCA1OC40ODkgLTIwLjUzNCA1Ni4zNzkgLTIxLjQ4NCA1My41MjkgTCAtMjUuNDY0IDUzLjUyOSBMIC0yNS40NjQgNTYuNjE5IEMgLTIzLjQ5NCA2MC41MzkgLTE5LjQ0NCA2My4yMzkgLTE0Ljc1NCA2My4yMzkgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjcuMDA5IC0zOS4yMzkpIi8+PHBhdGggZmlsbD0iI0ZCQkMwNSIgZD0iTSAtMjEuNDg0IDUzLjUyOSBDIC0yMS43MzQgNTIuODA5IC0yMS44NjQgNTIuMDM5IC0yMS44NjQgNTEuMjM5IEMgLTIxLjg2NCA1MC40MzkgLTIxLjcyNCA0OS42NjkgLTIxLjQ4NCA0OC45NDkgTCAtMjEuNDg0IDQ1Ljg1OSBMIC0yNS40NjQgNDUuODU5IEMgLTI2LjI4NCA0Ny40NzkgLTI2Ljc1NCA0OS4yOTkgLTI2Ljc1NCA1MS4yMzkgQyAtMjYuNzU0IDUzLjE3OSAtMjYuMjg0IDU0Ljk5OSAtMjUuNDY0IDU2LjYxOSBMIC0yMS40ODQgNTMuNTI5IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3LjAwOSAtMzkuMjM5KSIvPjxwYXRoIGZpbGw9IiNFQTQzMzUiIGQ9Ik0gLTE0Ljc1NCA0My45ODkgQyAtMTIuOTg0IDQzLjk4OSAtMTEuNDA0IDQ0LjU5OSAtMTAuMTU0IDQ1Ljc4OSBMIC02LjczNCA0Mi4zNjkgQyAtOC44MDQgNDAuNDI5IC0xMS41MTQgMzkuMjM5IC0xNC43NTQgMzkuMjM5IEMgLTE5LjQ0NCAzOS4yMzkgLTIzLjQ5NCA0MS45MzkgLTI1LjQ2NCA0NS44NTkgTCAtMjEuNDg0IDQ4Ljk0OSBDIC0yMC41MzQgNDYuMDk5IC0xNy44ODQgNDMuOTg5IC0xNC43NTQgNDMuOTg5IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3LjAwOSAtMzkuMjM5KSIvPjwvc3ZnPg=='); --icon-GristLogo: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDEiIGhlaWdodD0iMzgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgTDUuMDE1NTg3NDMsMC4wOTg1OTE3MzQ3IEMyLjMxNzYzNDkxLDAuMDk4NTkxNzM0NyAwLjEyNzMwMTg2LDIuMjg4OTIwMDkgMC4xMjczMDE4Niw0Ljk4Njg0MTgzIEwwLjEyNzMwMTg2LDkuODU3ODg5NzIgTDYuMDIwMjM5OTUsOS44NTc4ODk3MiBDOC42OTI3ODUwMyw5Ljg1Nzg4OTcyIDEwLjg2MjQ3NDUsNy42ODgxMDEzMSAxMC44NjI0NzQ1LDUuMDE1NTk3NzUgTDEwLjg2MjQ3NDUsMC4wOTg1OTE3MzQ3IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI4LjEzNCAxLjE2MikiIGZpbGw9IiMyQ0IwQUYiLz48cGF0aCBkPSJNMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgTDUuMDE1NTg3NDMsMC4wOTg1OTE3MzQ3IEMyLjMxNzYzNDkxLDAuMDk4NTkxNzM0NyAwLjEyNzMwMTg2LDIuMjg4OTIwMDkgMC4xMjczMDE4Niw0Ljk4Njg0MTgzIEwwLjEyNzMwMTg2LDkuODU3ODg5NzIgTDYuMDIwMjM5OTUsOS44NTc4ODk3MiBDOC42OTI3ODUwMyw5Ljg1Nzg4OTcyIDEwLjg2MjQ3NDUsNy42ODgxMDEzMSAxMC44NjI0NzQ1LDUuMDE1NTk3NzUgTDEwLjg2MjQ3NDUsMC4wOTg1OTE3MzQ3IFoiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDI1LjU3IDEuMTYyKSIgZmlsbD0iIzJDQjBBRiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMTIuMDE1IDEzLjc0KSIgZmlsbD0iI0Y5QUU0MSIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMTIuMDE1IDI2LjMxOSkiIGZpbGw9IiNGOUFFNDEiLz48cGF0aCBkPSJNMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgTDUuMDE1NTg3NDMsMC4wOTg1OTE3MzQ3IEMyLjMxNzYzNDkxLDAuMDk4NTkxNzM0NyAwLjEyNzMwMTg2LDIuMjg4OTIwMDkgMC4xMjczMDE4Niw0Ljk4Njg0MTgzIEwwLjEyNzMwMTg2LDkuODU3ODg5NzIgTDYuMDIwMjM5OTUsOS44NTc4ODk3MiBDOC42OTI3ODUwMyw5Ljg1Nzg4OTcyIDEwLjg2MjQ3NDUsNy42ODgxMDEzMSAxMC44NjI0NzQ1LDUuMDE1NTk3NzUgTDEwLjg2MjQ3NDUsMC4wOTg1OTE3MzQ3IFoiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDI1LjU3IDEzLjc0KSIgZmlsbD0iI0QyRDJEMiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjguMTM0IDEzLjc0KSIgZmlsbD0iI0QyRDJEMiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMjUuNTcgMjYuMzE5KSIgZmlsbD0iI0QyRDJEMiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjguMTM0IDI2LjMxOSkiIGZpbGw9IiNEMkQyRDIiLz48L2c+PC9zdmc+'); --icon-ThumbPreview: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDYiIGhlaWdodD0iNDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxwYXRoIGQ9Ik00LjA1NzYxNzE5LDYuMzY4MTY0MDYgTDQuMDU3NjE3MTksNS4xNjIxMDkzOCBMNy4xNzI4NTE1Niw1LjE2MjEwOTM4IEw3LjE3Mjg1MTU2LDguMDEzNjcxODggQzYuODcwMTE1NjcsOC4zMDY2NDIwOSA2LjQzMTQ4MDc0LDguNTY0NjE0NzcgNS44NTY5MzM1OSw4Ljc4NzU5NzY2IEM1LjI4MjM4NjQ1LDkuMDEwNTgwNTQgNC43MDA1MjM3OCw5LjEyMjA3MDMxIDQuMTExMzI4MTIsOS4xMjIwNzAzMSBDMy4zNjI2MjY0Niw5LjEyMjA3MDMxIDIuNzA5OTYzNzIsOC45NjUwMDgwOCAyLjE1MzMyMDMxLDguNjUwODc4OTEgQzEuNTk2Njc2OSw4LjMzNjc0OTczIDEuMTc4Mzg2ODIsNy44ODc1MzU0NyAwLjg5ODQzNzUsNy4zMDMyMjI2NiBDMC42MTg0ODgxODQsNi43MTg5MDk4NCAwLjQ3ODUxNTYyNSw2LjA4MzMzNjc3IDAuNDc4NTE1NjI1LDUuMzk2NDg0MzggQzAuNDc4NTE1NjI1LDQuNjUxMDM3OTQgMC42MzQ3NjQwNjIsMy45ODg2MDk2NyAwLjk0NzI2NTYyNSwzLjQwOTE3OTY5IEMxLjI1OTc2NzE5LDIuODI5NzQ5NzEgMS43MTcxMTkzOCwyLjM4NTQxODIxIDIuMzE5MzM1OTQsMi4wNzYxNzE4OCBDMi43NzgzMjI2MSwxLjgzODU0MDQ4IDMuMzQ5NjA1OTYsMS43MTk3MjY1NiA0LjAzMzIwMzEyLDEuNzE5NzI2NTYgQzQuOTIxODc5NDQsMS43MTk3MjY1NiA1LjYxNjA0NTY4LDEuOTA2MDg1MzggNi4xMTU3MjI2NiwyLjI3ODgwODU5IEM2LjYxNTM5OTYzLDIuNjUxNTMxODEgNi45MzY4NDgyNCwzLjE2NjY2MzM4IDcuMDgwMDc4MTIsMy44MjQyMTg3NSBMNS42NDQ1MzEyNSw0LjA5Mjc3MzQ0IEM1LjU0MzYxOTI5LDMuNzQxMjA5MTggNS4zNTQwMDUzLDMuNDYzNzA1NDQgNS4wNzU2ODM1OSwzLjI2MDI1MzkxIEM0Ljc5NzM2MTg5LDMuMDU2ODAyMzcgNC40NDk4NzE4NywyLjk1NTA3ODEyIDQuMDMzMjAzMTIsMi45NTUwNzgxMiBDMy40MDE2ODk1NSwyLjk1NTA3ODEyIDIuODk5NTc4NjksMy4xNTUyNzE0NCAyLjUyNjg1NTQ3LDMuNTU1NjY0MDYgQzIuMTU0MTMyMjUsMy45NTYwNTY2OSAxLjk2Nzc3MzQ0LDQuNTUwMTI2MjcgMS45Njc3NzM0NCw1LjMzNzg5MDYyIEMxLjk2Nzc3MzQ0LDYuMTg3NTA0MjUgMi4xNTY1NzM2Myw2LjgyNDcwNDkxIDIuNTM0MTc5NjksNy4yNDk1MTE3MiBDMi45MTE3ODU3NCw3LjY3NDMxODUzIDMuNDA2NTcyNDYsNy44ODY3MTg3NSA0LjAxODU1NDY5LDcuODg2NzE4NzUgQzQuMzIxMjkwNTgsNy44ODY3MTg3NSA0LjYyNDgzNTcyLDcuODI3MzExNzkgNC45MjkxOTkyMiw3LjcwODQ5NjA5IEM1LjIzMzU2MjcyLDcuNTg5NjgwNCA1LjQ5NDc5MDU4LDcuNDQ1NjM4ODcgNS43MTI4OTA2Miw3LjI3NjM2NzE5IEw1LjcxMjg5MDYyLDYuMzY4MTY0MDYgTDQuMDU3NjE3MTksNi4zNjgxNjQwNiBaIE05LjIxMDc0MjE4LDkgTDkuMjEwNzQyMTgsMS44NDE3OTY4OCBMMTIuMjUyNzM0NCwxLjg0MTc5Njg4IEMxMy4wMTc3MTIxLDEuODQxNzk2ODggMTMuNTczNTMzNCwxLjkwNjA4NjYgMTMuOTIwMjE0OCwyLjAzNDY2Nzk3IEMxNC4yNjY4OTYzLDIuMTYzMjQ5MzQgMTQuNTQ0NCwyLjM5MTkyNTQ0IDE0Ljc1MjczNDQsMi43MjA3MDMxMiBDMTQuOTYxMDY4NywzLjA0OTQ4MDgxIDE1LjA2NTIzNDQsMy40MjU0NTM2MSAxNS4wNjUyMzQ0LDMuODQ4NjMyODEgQzE1LjA2NTIzNDQsNC4zODU3NDQ4NyAxNC45MDczNTgzLDQuODI5MjYyNTcgMTQuNTkxNjAxNiw1LjE3OTE5OTIyIEMxNC4yNzU4NDQ4LDUuNTI5MTM1ODYgMTMuODAzODQ0Myw1Ljc0OTY3NDAyIDEzLjE3NTU4NTksNS44NDA4MjAzMSBDMTMuNDg4MDg3NSw2LjAyMzExMjg5IDEzLjc0NjA2MDIsNi4yMjMzMDYyIDEzLjk0OTUxMTcsNi40NDE0MDYyNSBDMTQuMTUyOTYzMiw2LjY1OTUwNjMgMTQuNDI3MjExOCw3LjA0Njg3MjIyIDE0Ljc3MjI2NTYsNy42MDM1MTU2MiBMMTUuNjQ2Mjg5MSw5IEwxMy45MTc3NzM0LDkgTDEyLjg3Mjg1MTYsNy40NDIzODI4MSBDMTIuNTAxNzU1OSw2Ljg4NTczOTQgMTIuMjQ3ODUyMiw2LjUzNDk5NDIxIDEyLjExMTEzMjgsNi4zOTAxMzY3MiBDMTEuOTc0NDEzNCw2LjI0NTI3OTIyIDExLjgyOTU1OCw2LjE0NTk5NjM2IDExLjY3NjU2MjUsNi4wOTIyODUxNiBDMTEuNTIzNTY2OSw2LjAzODU3Mzk1IDExLjI4MTA1NjMsNi4wMTE3MTg3NSAxMC45NDkwMjM0LDYuMDExNzE4NzUgTDEwLjY1NjA1NDcsNi4wMTE3MTg3NSBMMTAuNjU2MDU0Nyw5IEw5LjIxMDc0MjE4LDkgWiBNMTAuNjU2MDU0Nyw0Ljg2OTE0MDYyIEwxMS43MjUzOTA2LDQuODY5MTQwNjIgQzEyLjQxODc1MzUsNC44NjkxNDA2MiAxMi44NTE2OTE4LDQuODM5ODQ0MDQgMTMuMDI0MjE4Nyw0Ljc4MTI1IEMxMy4xOTY3NDU2LDQuNzIyNjU1OTYgMTMuMzMxODM1NCw0LjYyMTc0NTUxIDEzLjQyOTQ5MjIsNC40Nzg1MTU2MiBDMTMuNTI3MTQ4OSw0LjMzNTI4NTc0IDEzLjU3NTk3NjYsNC4xNTYyNTEwNyAxMy41NzU5NzY2LDMuOTQxNDA2MjUgQzEzLjU3NTk3NjYsMy43MDA1MTk2MyAxMy41MTE2ODY4LDMuNTA2MDIyODggMTMuMzgzMTA1NSwzLjM1NzkxMDE2IEMxMy4yNTQ1MjQxLDMuMjA5Nzk3NDQgMTMuMDczMDQ4LDMuMTE2MjExMTMgMTIuODM4NjcxOSwzLjA3NzE0ODQ0IEMxMi43MjE0ODM4LDMuMDYwODcyMzEgMTIuMzY5OTI0OCwzLjA1MjczNDM4IDExLjc4Mzk4NDQsMy4wNTI3MzQzOCBMMTAuNjU2MDU0NywzLjA1MjczNDM4IEwxMC42NTYwNTQ3LDQuODY5MTQwNjIgWiBNMTcuMDgzNTkzNyw5IEwxNy4wODM1OTM3LDEuODQxNzk2ODggTDE4LjUyODkwNjIsMS44NDE3OTY4OCBMMTguNTI4OTA2Miw5IEwxNy4wODM1OTM3LDkgWiBNMjAuMjM5NjQ4NCw2LjY3MDg5ODQ0IEwyMS42NDU4OTg0LDYuNTM0MTc5NjkgQzIxLjczMDUzNDIsNy4wMDYxODcyNiAyMS45MDIyNDQ4LDcuMzUyODYzNDggMjIuMTYxMDM1MSw3LjU3NDIxODc1IEMyMi40MTk4MjU1LDcuNzk1NTc0MDIgMjIuNzY4OTQzMSw3LjkwNjI1IDIzLjIwODM5ODQsNy45MDYyNSBDMjMuNjczODk1NSw3LjkwNjI1IDI0LjAyNDY0MDcsNy44MDc3ODA5MyAyNC4yNjA2NDQ1LDcuNjEwODM5ODQgQzI0LjQ5NjY0ODMsNy40MTM4OTg3NSAyNC42MTQ2NDg0LDcuMTgzNTk1MDcgMjQuNjE0NjQ4NCw2LjkxOTkyMTg4IEMyNC42MTQ2NDg0LDYuNzUwNjUwMiAyNC41NjUwMDcsNi42MDY2MDg2NyAyNC40NjU3MjI2LDYuNDg3NzkyOTcgQzI0LjM2NjQzODMsNi4zNjg5NzcyNyAyNC4xOTMxMDAyLDYuMjY1NjI1NDQgMjMuOTQ1NzAzMSw2LjE3NzczNDM4IEMyMy43NzY0MzE0LDYuMTE5MTQwMzMgMjMuMzkwNjkzMSw2LjAxNDk3NDcxIDIyLjc4ODQ3NjUsNS44NjUyMzQzOCBDMjIuMDEzNzMzMSw1LjY3MzE3NjEyIDIxLjQ3MDExODcsNS40MzcxNzU4OCAyMS4xNTc2MTcyLDUuMTU3MjI2NTYgQzIwLjcxODE2MTgsNC43NjMzNDQzOCAyMC40OTg0Mzc1LDQuMjgzMjA1OTYgMjAuNDk4NDM3NSwzLjcxNjc5Njg4IEMyMC40OTg0Mzc1LDMuMzUyMjExNzIgMjAuNjAxNzg5MywzLjAxMTIzMjA2IDIwLjgwODQ5NjEsMi42OTM4NDc2NiBDMjEuMDE1MjAyOCwyLjM3NjQ2MzI2IDIxLjMxMzA1MTQsMi4xMzQ3NjY0NiAyMS43MDIwNTA3LDEuOTY4NzUgQzIyLjA5MTA1MDEsMS44MDI3MzM1NCAyMi41NjA2MDkyLDEuNzE5NzI2NTYgMjMuMTEwNzQyMiwxLjcxOTcyNjU2IEMyNC4wMDkxODQxLDEuNzE5NzI2NTYgMjQuNjg1NDQ2OSwxLjkxNjY2NDcgMjUuMTM5NTUwNywyLjMxMDU0Njg4IEMyNS41OTM2NTQ2LDIuNzA0NDI5MDUgMjUuODMyMDk2MiwzLjIzMDEzOTk0IDI1Ljg1NDg4MjgsMy44ODc2OTUzMSBMMjQuNDA5NTcwMywzLjk1MTE3MTg4IEMyNC4zNDc3MjEsMy41ODMzMzE0OSAyNC4yMTUwNzI2LDMuMzE4ODQ4NDYgMjQuMDExNjIxMSwzLjE1NzcxNDg0IEMyMy44MDgxNjk1LDIuOTk2NTgxMjMgMjMuNTAyOTk2OCwyLjkxNjAxNTYyIDIzLjA5NjA5MzcsMi45MTYwMTU2MiBDMjIuNjc2MTY5NywyLjkxNjAxNTYyIDIyLjM0NzM5NywzLjAwMjI3Nzc4IDIyLjEwOTc2NTYsMy4xNzQ4MDQ2OSBDMjEuOTU2NzcsMy4yODU0ODIzMiAyMS44ODAyNzM0LDMuNDMzNTkyODIgMjEuODgwMjczNCwzLjYxOTE0MDYyIEMyMS44ODAyNzM0LDMuNzg4NDEyMyAyMS45NTE4ODczLDMuOTMzMjY3NjMgMjIuMDk1MTE3Miw0LjA1MzcxMDk0IEMyMi4yNzc0MDk3LDQuMjA2NzA2NDkgMjIuNzIwMTEzNiw0LjM2NjIxMDExIDIzLjQyMzI0MjIsNC41MzIyMjY1NiBDMjQuMTI2MzcwNyw0LjY5ODI0MzAyIDI0LjY0NjM4NSw0Ljg2OTk1MzU0IDI0Ljk4MzMwMDcsNS4wNDczNjMyOCBDMjUuMzIwMjE2NSw1LjIyNDc3MzAyIDI1LjU4Mzg4NTcsNS40NjcyODM2MiAyNS43NzQzMTY0LDUuNzc0OTAyMzQgQzI1Ljk2NDc0Nyw2LjA4MjUyMTA3IDI2LjA1OTk2MDksNi40NjI1NjI4NCAyNi4wNTk5NjA5LDYuOTE1MDM5MDYgQzI2LjA1OTk2MDksNy4zMjUxOTczNiAyNS45NDYwMjk3LDcuNzA5MzA4MTEgMjUuNzE4MTY0LDguMDY3MzgyODEgQzI1LjQ5MDI5ODMsOC40MjU0NTc1MiAyNS4xNjgwMzU5LDguNjkxNTY4MTQgMjQuNzUxMzY3Miw4Ljg2NTcyMjY2IEMyNC4zMzQ2OTg0LDkuMDM5ODc3MTcgMjMuODE1NDk3OSw5LjEyNjk1MzEyIDIzLjE5Mzc1LDkuMTI2OTUzMTIgQzIyLjI4ODc5NzUsOS4xMjY5NTMxMiAyMS41OTM4MTc1LDguOTE3ODA4MDggMjEuMTA4Nzg5LDguNDk5NTExNzIgQzIwLjYyMzc2MDYsOC4wODEyMTUzNiAyMC4zMzQwNDk5LDcuNDcxNjgzNjkgMjAuMjM5NjQ4NCw2LjY3MDg5ODQ0IFogTTI5LjU4NzEwOTMsOSBMMjkuNTg3MTA5MywzLjA1MjczNDM4IEwyNy40NjMwODU5LDMuMDUyNzM0MzggTDI3LjQ2MzA4NTksMS44NDE3OTY4OCBMMzMuMTUxNTYyNSwxLjg0MTc5Njg4IEwzMy4xNTE1NjI1LDMuMDUyNzM0MzggTDMxLjAzMjQyMTgsMy4wNTI3MzQzOCBMMzEuMDMyNDIxOCw5IEwyOS41ODcxMDkzLDkgWiIgaWQ9ImEiLz48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBvcGFjaXR5PSIuOSI+PHBhdGggZD0iTTAgMEg0OFY0OEgweiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEgLTEpIi8+PHBhdGggZmlsbD0iIzBFNTEyQiIgZD0iTTQzIDI2TDMgMjYgMCAyMiA0IDE4IDQyIDE4IDQ2IDIyeiIvPjxwYXRoIGQ9Ik00MCw0NiBMNiw0NiBDNC44OTUsNDYgNCw0NS4xMDUgNCw0NCBMNCwyIEM0LDAuODk1IDQuODk1LDAgNiwwIEwzMCwwIEw0MiwxMiBMNDIsNDQgQzQyLDQ1LjEwNSA0MS4xMDUsNDYgNDAsNDYgWiIgZmlsbD0iI0U2RTZFNiIvPjxwYXRoIGQ9Ik0zMCwwIEwzMCwxMCBDMzAsMTEuMTA1IDMwLjg5NSwxMiAzMiwxMiBMNDIsMTIgTDMwLDAgWiIgZmlsbD0iI0IzQjNCMyIvPjxwYXRoIGQ9Ik00NCw0MCBMMiw0MCBDMC44OTUsNDAgMCwzOS4xMDUgMCwzOCBMMCwyMiBMNDYsMjIgTDQ2LDM4IEM0NiwzOS4xMDUgNDUuMTA1LDQwIDQ0LDQwIFoiIGZpbGw9IiMxNkIzNzgiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2IDI1KSI+PHVzZSBmaWxsPSIjMDAwIiB4bGluazpocmVmPSIjYSIvPjx1c2UgZmlsbD0iI0ZGRiIgeGxpbms6aHJlZj0iI2EiLz48L2c+PC9nPjwvc3ZnPg=='); @@ -134,13 +134,17 @@ --icon-Separator: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEwNjFfNzU4OCkiIGZpbGw9IiMwMDAiPjxwYXRoIGQ9Ik0xLjMzMyA3SDE0LjY2NlY4LjMzM0gxLjMzM3pNMCA1SDEuMzMzVjEwLjMzM0gwek0xNC42NjcgNUgxNlYxMC4zMzNIMTQuNjY3eiIvPjwvZz48ZGVmcz48Y2xpcFBhdGggaWQ9ImNsaXAwXzEwNjFfNzU4OCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTAgMEgxNlYxNkgweiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg=='); --icon-Settings: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuOTgwNDcxOSwzLjI3MzM2NTEyIEM0LjgxNDE0NDY1LDIuNTc4NDA2ODQgNS44NTY2MjI5MSwyLjEyNTQ1Njc0IDcsMi4wMjI0MjE1MSBMNywwLjUgQzcsMC4yMjM4NTc2MjUgNy4yMjM4NTc2MywwIDcuNSwwIEM3Ljc3NjE0MjM3LDAgOCwwLjIyMzg1NzYyNSA4LDAuNSBMOCwyLjAyMjQyMTUxIEM5LjE0MzM3NzA5LDIuMTI1NDU2NzQgMTAuMTg1ODU1NCwyLjU3ODQwNjg0IDExLjAxOTUyODEsMy4yNzMzNjUxMiBMMTIuMDk2NDQ2NiwyLjE5NjQ0NjYxIEMxMi4yOTE3MDg4LDIuMDAxMTg0NDYgMTIuNjA4MjkxMiwyLjAwMTE4NDQ2IDEyLjgwMzU1MzQsMi4xOTY0NDY2MSBDMTIuOTk4ODE1NSwyLjM5MTcwODc2IDEyLjk5ODgxNTUsMi43MDgyOTEyNCAxMi44MDM1NTM0LDIuOTAzNTUzMzkgTDExLjcyNjYzNDksMy45ODA0NzE5IEMxMi40MjE1OTMyLDQuODE0MTQ0NjUgMTIuODc0NTQzMyw1Ljg1NjYyMjkxIDEyLjk3NzU3ODUsNyBMMTQuNSw3IEMxNC43NzYxNDI0LDcgMTUsNy4yMjM4NTc2MyAxNSw3LjUgQzE1LDcuNzc2MTQyMzcgMTQuNzc2MTQyNCw4IDE0LjUsOCBMMTIuOTc3NTc4NSw4IEMxMi44NzQ1NDMzLDkuMTQzMzc3MDkgMTIuNDIxNTkzMiwxMC4xODU4NTU0IDExLjcyNjYzNDksMTEuMDE5NTI4MSBMMTIuODAzNTUzNCwxMi4wOTY0NDY2IEMxMi45OTg4MTU1LDEyLjI5MTcwODggMTIuOTk4ODE1NSwxMi42MDgyOTEyIDEyLjgwMzU1MzQsMTIuODAzNTUzNCBDMTIuNjA4MjkxMiwxMi45OTg4MTU1IDEyLjI5MTcwODgsMTIuOTk4ODE1NSAxMi4wOTY0NDY2LDEyLjgwMzU1MzQgTDExLjAxOTUyODEsMTEuNzI2NjM0OSBDMTAuMTg1ODU1NCwxMi40MjE1OTMyIDkuMTQzMzc3MDksMTIuODc0NTQzMyA4LDEyLjk3NzU3ODUgTDgsMTQuNSBDOCwxNC43NzYxNDI0IDcuNzc2MTQyMzcsMTUgNy41LDE1IEM3LjIyMzg1NzYzLDE1IDcsMTQuNzc2MTQyNCA3LDE0LjUgTDcsMTIuOTc3NTc4NSBDNS44NTY2MjI5MSwxMi44NzQ1NDMzIDQuODE0MTQ0NjUsMTIuNDIxNTkzMiAzLjk4MDQ3MTksMTEuNzI2NjM0OSBMMi45MDM1NTMzOSwxMi44MDM1NTM0IEMyLjcwODI5MTI0LDEyLjk5ODgxNTUgMi4zOTE3MDg3NiwxMi45OTg4MTU1IDIuMTk2NDQ2NjEsMTIuODAzNTUzNCBDMi4wMDExODQ0NiwxMi42MDgyOTEyIDIuMDAxMTg0NDYsMTIuMjkxNzA4OCAyLjE5NjQ0NjYxLDEyLjA5NjQ0NjYgTDMuMjczMzY1MTIsMTEuMDE5NTI4MSBDMi41Nzg0MDY4NCwxMC4xODU4NTU0IDIuMTI1NDU2NzQsOS4xNDMzNzcwOSAyLjAyMjQyMTUxLDggTDAuNSw4IEMwLjIyMzg1NzYyNSw4IDAsNy43NzYxNDIzNyAwLDcuNSBDMCw3LjIyMzg1NzYzIDAuMjIzODU3NjI1LDcgMC41LDcgTDIuMDIyNDIxNTEsNyBDMi4xMjU0NTY3NCw1Ljg1NjYyMjkxIDIuNTc4NDA2ODQsNC44MTQxNDQ2NSAzLjI3MzM2NTEyLDMuOTgwNDcxOSBMMi4xOTY0NDY2MSwyLjkwMzU1MzM5IEMyLjAwMTE4NDQ2LDIuNzA4MjkxMjQgMi4wMDExODQ0NiwyLjM5MTcwODc2IDIuMTk2NDQ2NjEsMi4xOTY0NDY2MSBDMi4zOTE3MDg3NiwyLjAwMTE4NDQ2IDIuNzA4MjkxMjQsMi4wMDExODQ0NiAyLjkwMzU1MzM5LDIuMTk2NDQ2NjEgTDMuOTgwNDcxOSwzLjI3MzM2NTEyIFogTTcuNSwxMCBDNi4xMTkyODgxMywxMCA1LDguODgwNzExODcgNSw3LjUgQzUsNi4xMTkyODgxMyA2LjExOTI4ODEzLDUgNy41LDUgQzguODgwNzExODcsNSAxMCw2LjExOTI4ODEzIDEwLDcuNSBDMTAsOC44ODA3MTE4NyA4Ljg4MDcxMTg3LDEwIDcuNSwxMCBaIE03LjUsOSBDOC4zMjg0MjcxMiw5IDksOC4zMjg0MjcxMiA5LDcuNSBDOSw2LjY3MTU3Mjg4IDguMzI4NDI3MTIsNiA3LjUsNiBDNi42NzE1NzI4OCw2IDYsNi42NzE1NzI4OCA2LDcuNSBDNiw4LjMyODQyNzEyIDYuNjcxNTcyODgsOSA3LjUsOSBaIE03LjUsMTIgQzkuOTg1MjgxMzcsMTIgMTIsOS45ODUyODEzNyAxMiw3LjUgQzEyLDUuMDE0NzE4NjMgOS45ODUyODEzNywzIDcuNSwzIEM1LjAxNDcxODYzLDMgMyw1LjAxNDcxODYzIDMsNy41IEMzLDkuOTg1MjgxMzcgNS4wMTQ3MTg2MywxMiA3LjUsMTIgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-Share: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYuNzc0MTEwMDUsOS4xNDQxMzAyOSBMOS43NTY4Nzg3NiwxMS4wMDc4NjUxIEMxMC4zMDY0MDY1LDEwLjM4OTU1MjMgMTEuMTA3NzEwOSwxMCAxMiwxMCBDMTMuNjU2ODU0MiwxMCAxNSwxMS4zNDMxNDU4IDE1LDEzIEMxNSwxNC42NTY4NTQyIDEzLjY1Njg1NDIsMTYgMTIsMTYgQzEwLjM0MzE0NTgsMTYgOSwxNC42NTY4NTQyIDksMTMgQzksMTIuNTk0NjU3MiA5LjA4MDM4OTUzLDEyLjIwODA5MDQgOS4yMjYwOTUwNywxMS44NTUzNzMgTDYuMjQzNDc4MjQsOS45OTE3MzMxNyBDNS42OTM5NDA1NSwxMC42MTAyNzkgNC44OTI0ODIzOSwxMSA0LDExIEMyLjM0MzE0NTc1LDExIDEsOS42NTY4NTQyNSAxLDggQzEsNi4zNDMxNDU3NSAyLjM0MzE0NTc1LDUgNCw1IEM0Ljg5MjQ4MjM5LDUgNS42OTM5NDA1NSw1LjM4OTcyMTAzIDYuMjQzNDc4MjQsNi4wMDgyNjY4MyBMOS4yMjYwOTUwNyw0LjE0NDYyNjk2IEM5LjA4MDM4OTUzLDMuNzkxOTA5NjMgOSwzLjQwNTM0MjggOSwzIEM5LDEuMzQzMTQ1NzUgMTAuMzQzMTQ1OCwwIDEyLDAgQzEzLjY1Njg1NDIsMCAxNSwxLjM0MzE0NTc1IDE1LDMgQzE1LDQuNjU2ODU0MjUgMTMuNjU2ODU0Miw2IDEyLDYgQzExLjEwNzcxMDksNiAxMC4zMDY0MDY1LDUuNjEwNDQ3NzMgOS43NTY4Nzg3Niw0Ljk5MjEzNDkzIEw2Ljc3NDExMDA1LDYuODU1ODY5NzEgQzYuOTE5Njg1OTIsNy4yMDg0NTMyNSA3LDcuNTk0ODQ3NDQgNyw4IEM3LDguNDA1MTUyNTYgNi45MTk2ODU5Miw4Ljc5MTU0Njc1IDYuNzc0MTEwMDUsOS4xNDQxMzAyOSBaIE01LjcwNDEyNDg1LDkuMDQ3NDE3MTYgQzUuODkxNzYwNjYsOC43NDI3ODczNCA2LDguMzg0MDM0IDYsOCBDNiw3LjYxNTk2NiA1Ljg5MTc2MDY2LDcuMjU3MjEyNjYgNS43MDQxMjQ4NSw2Ljk1MjU4Mjg0IEM1LjcwMTM1MzQ2LDYuOTQ4NDI1NzcgNS42OTg2MzQ0Miw2Ljk0NDIxNDMyIDUuNjk1OTY5MTgsNi45Mzk5NDg4IEM1LjY5MzQ2MjQ3LDYuOTM1OTM2OTkgNS42OTEwMTk1OSw2LjkzMTkwMzM4IDUuNjg4NjQwMjksNi45Mjc4NDkxNSBDNS4zMzM3NDk5MSw2LjM3MDA2MTY1IDQuNzEwMDkxMjIsNiA0LDYgQzIuODk1NDMwNSw2IDIsNi44OTU0MzA1IDIsOCBDMiw5LjEwNDU2OTUgMi44OTU0MzA1LDEwIDQsMTAgQzQuNzEwMDkxMjIsMTAgNS4zMzM3NDk5MSw5LjYyOTkzODM1IDUuNjg4NjQwMjksOS4wNzIxNTA4NSBDNS42OTEwMTk1OSw5LjA2ODA5NjYyIDUuNjkzNDYyNDcsOS4wNjQwNjMwMSA1LjY5NTk2OTE4LDkuMDYwMDUxMiBDNS42OTg2MzQ0Miw5LjA1NTc4NTY4IDUuNzAxMzUzNDYsOS4wNTE1NzQyMyA1LjcwNDEyNDg1LDkuMDQ3NDE3MTYgWiBNMTAuMzE5OTA5MiwxMS45MTQ1MjgzIEMxMC4zMTUyMjY4LDExLjkyMzA4OTcgMTAuMzEwMjY4MSwxMS45MzE1NjY5IDEwLjMwNTAzMDgsMTEuOTM5OTQ4OCBDMTAuMjk5NTk2LDExLjk0ODY0NjggMTAuMjkzOTM3NiwxMS45NTcxMTk5IDEwLjI4ODA2NzYsMTEuOTY1MzY1NSBDMTAuMTA1MjQsMTIuMjY3MjI4NiAxMCwxMi42MjEzMjQzIDEwLDEzIEMxMCwxNC4xMDQ1Njk1IDEwLjg5NTQzMDUsMTUgMTIsMTUgQzEzLjEwNDU2OTUsMTUgMTQsMTQuMTA0NTY5NSAxNCwxMyBDMTQsMTEuODk1NDMwNSAxMy4xMDQ1Njk1LDExIDEyLDExIEMxMS4yOTU1NzY3LDExIDEwLjY3NjIxMTcsMTEuMzY0MTc3NSAxMC4zMTk5MDkyLDExLjkxNDUyODMgWiBNMTAuMjg4MDY3Niw0LjAzNDYzNDU0IEMxMC4yOTM5Mzc2LDQuMDQyODgwMDkgMTAuMjk5NTk2LDQuMDUxMzUzMjUgMTAuMzA1MDMwOCw0LjA2MDA1MTIgQzEwLjMxMDI2ODEsNC4wNjg0MzMxNSAxMC4zMTUyMjY4LDQuMDc2OTEwMjUgMTAuMzE5OTA5Miw0LjA4NTQ3MTY4IEMxMC42NzYyMTE3LDQuNjM1ODIyNDUgMTEuMjk1NTc2Nyw1IDEyLDUgQzEzLjEwNDU2OTUsNSAxNCw0LjEwNDU2OTUgMTQsMyBDMTQsMS44OTU0MzA1IDEzLjEwNDU2OTUsMSAxMiwxIEMxMC44OTU0MzA1LDEgMTAsMS44OTU0MzA1IDEwLDMgQzEwLDMuMzc4Njc1NzQgMTAuMTA1MjQsMy43MzI3NzEzNyAxMC4yODgwNjc2LDQuMDM0NjM0NTQgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); + --icon-Skip: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNy4wNjI1IDMuODIxMDRIMTUuODEyNVYxNi4zMjFIMTcuMDYyNVYzLjgyMTA0Wk0xNC4yNjQzIDEwLjk2NDhDMTQuNTc4NSAxMC43NzY2IDE0Ljc3MDggMTAuNDM3MyAxNC43NzA4IDEwLjA3MUMxNC43NzA4IDkuNzA0OCAxNC41Nzg1IDkuMzY1NDYgMTQuMjY0MyA5LjE3NzMyTDUuODI2NzcgNC4xMjU1MUM1LjUwNDkzIDMuOTMyODEgNS4xMDQzNSAzLjkyODA0IDQuNzc4MDIgNC4xMTMwMUM0LjQ1MTY4IDQuMjk3OTggNC4yNSA0LjY0NDEyIDQuMjUgNS4wMTkyM1YxNS4xMjI5QzQuMjUgMTUuNDk4IDQuNDUxNjggMTUuODQ0MSA0Ljc3ODAyIDE2LjAyOTFDNS4xMDQzNSAxNi4yMTQgNS41MDQ5MyAxNi4yMDkzIDUuODI2NzcgMTYuMDE2NkwxNC4yNjQzIDEwLjk2NDhaTTUuMjkxNjcgNS4wMTkyM0wxMy43MjkyIDEwLjA3MUw1LjI5MTY3IDE1LjEyMjlMNS4yOTE2NyA1LjAxOTIzWiIgZmlsbD0iIzE2QjM3OCIvPjwvc3ZnPg=='); --icon-Sort: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuOTk5MjUsMy43MDY1NTM3NyBMMy45OTkyNSwxMy40OTkyNSBDMy45OTkyNSwxMy43NzUyNSAzLjc3NTI1LDEzLjk5OTI1IDMuNDk5MjUsMTMuOTk5MjUgQzMuMjIzMjUsMTMuOTk5MjUgMi45OTkyNSwxMy43NzUyNSAyLjk5OTI1LDEzLjQ5OTI1IEwyLjk5OTI1LDMuNzA2NTUzNzcgTDEuODUzMjUsNC44NTMyNSBDMS42NTgyNSw1LjA0ODI1IDEuMzQxMjUsNS4wNDgyNSAxLjE0NjI1LDQuODUzMjUgQzAuOTUxMjUsNC42NTgyNSAwLjk1MTI1LDQuMzQxMjUgMS4xNDYyNSw0LjE0NjI1IEwzLjE0NjI1LDIuMTQ2MjUgQzMuMzQxMjUsMS45NTEyNSAzLjY1ODI1LDEuOTUxMjUgMy44NTMyNSwyLjE0NjI1IEw1Ljg1MzI1LDQuMTQ2MjUgQzYuMDQ4MjUsNC4zNDEyNSA2LjA0ODI1LDQuNjU4MjUgNS44NTMyNSw0Ljg1MzI1IEM1Ljc1NTI1LDQuOTUwMjUgNS42MjcyNSw0Ljk5OTI1IDUuNDk5MjUsNC45OTkyNSBDNS4zNzEyNSw0Ljk5OTI1IDUuMjQzMjUsNC45NTAyNSA1LjE0NTI1LDQuODUzMjUgTDMuOTk5MjUsMy43MDY1NTM3NyBaIE04LjQ5OTI1LDMuOTk5MjUgTDcuNDk5MjUsMy45OTkyNSBDNy4yMjMyNSwzLjk5OTI1IDYuOTk5MjUsMy43NzUyNSA2Ljk5OTI1LDMuNDk5MjUgQzYuOTk5MjUsMy4yMjMyNSA3LjIyMzI1LDIuOTk5MjUgNy40OTkyNSwyLjk5OTI1IEw4LjQ5OTI1LDIuOTk5MjUgQzguNzc1MjUsMi45OTkyNSA4Ljk5OTI1LDMuMjIzMjUgOC45OTkyNSwzLjQ5OTI1IEM4Ljk5OTI1LDMuNzc1MjUgOC43NzUyNSwzLjk5OTI1IDguNDk5MjUsMy45OTkyNSBaIE0xMC40OTkyNSw2Ljk5OTI1IEw3LjQ5OTI1LDYuOTk5MjUgQzcuMjIzMjUsNi45OTkyNSA2Ljk5OTI1LDYuNzc1MjUgNi45OTkyNSw2LjQ5OTI1IEM2Ljk5OTI1LDYuMjIzMjUgNy4yMjMyNSw1Ljk5OTI1IDcuNDk5MjUsNS45OTkyNSBMMTAuNDk5MjUsNS45OTkyNSBDMTAuNzc1MjUsNS45OTkyNSAxMC45OTkyNSw2LjIyMzI1IDEwLjk5OTI1LDYuNDk5MjUgQzEwLjk5OTI1LDYuNzc1MjUgMTAuNzc1MjUsNi45OTkyNSAxMC40OTkyNSw2Ljk5OTI1IFogTTEyLjQ5OTI1LDkuOTk5MjUgTDcuNDk5MjUsOS45OTkyNSBDNy4yMjMyNSw5Ljk5OTI1IDYuOTk5MjUsOS43NzUyNSA2Ljk5OTI1LDkuNDk5MjUgQzYuOTk5MjUsOS4yMjMyNSA3LjIyMzI1LDguOTk5MjUgNy40OTkyNSw4Ljk5OTI1IEwxMi40OTkyNSw4Ljk5OTI1IEMxMi43NzUyNSw4Ljk5OTI1IDEyLjk5OTI1LDkuMjIzMjUgMTIuOTk5MjUsOS40OTkyNSBDMTIuOTk5MjUsOS43NzUyNSAxMi43NzUyNSw5Ljk5OTI1IDEyLjQ5OTI1LDkuOTk5MjUgWiBNMTQuNDk5MjUsMTIuOTk5MjUgTDcuNDk5MjUsMTIuOTk5MjUgQzcuMjIzMjUsMTIuOTk5MjUgNi45OTkyNSwxMi43NzUyNSA2Ljk5OTI1LDEyLjQ5OTI1IEM2Ljk5OTI1LDEyLjIyMzI1IDcuMjIzMjUsMTEuOTk5MjUgNy40OTkyNSwxMS45OTkyNSBMMTQuNDk5MjUsMTEuOTk5MjUgQzE0Ljc3NTI1LDExLjk5OTI1IDE0Ljk5OTI1LDEyLjIyMzI1IDE0Ljk5OTI1LDEyLjQ5OTI1IEMxNC45OTkyNSwxMi43NzUyNSAxNC43NzUyNSwxMi45OTkyNSAxNC40OTkyNSwxMi45OTkyNSBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMTYpIi8+PC9zdmc+'); --icon-Sparks: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDYiIGhlaWdodD0iNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTM0LjYyNDggMzAuMjE5QzMzLjU1NzUgMjkuNDYxMiAzMi4yODkzIDI5LjAzNjkgMzAuOTgwOCAyOUMyOS4wNDE4IDI5LjAzNiAyNi4wNzQ4IDI5Ljk5MiAyMy45OTk4IDM0Ljc0N1YxMy45ODdDMjMuOTk5OCAxMy43MjE4IDIzLjg5NDQgMTMuNDY3NCAyMy43MDY5IDEzLjI3OTlDMjMuNTE5NCAxMy4wOTI0IDIzLjI2NSAxMi45ODcgMjIuOTk5OCAxMi45ODdDMjIuNzM0NiAxMi45ODcgMjIuNDgwMiAxMy4wOTI0IDIyLjI5MjcgMTMuMjc5OUMyMi4xMDUyIDEzLjQ2NzQgMjEuOTk5OCAxMy43MjE4IDIxLjk5OTggMTMuOTg3VjM0Ljc0N0MxOS45MjQ4IDI5Ljk5MiAxNi45NTc4IDI5LjAzNiAxNS4wMTg4IDI5SDE0LjkyNDhDMTMuNjQzNyAyOS4wMjkyIDEyLjQwMzIgMjkuNDU1NSAxMS4zNzQ4IDMwLjIyQzExLjI3MjIgMzAuMzAyMSAxMS4xODY4IDMwLjQwMzUgMTEuMTIzNSAzMC41MTg2QzExLjA2MDIgMzAuNjMzNyAxMS4wMjAxIDMwLjc2MDEgMTEuMDA1NiAzMC44OTA3QzEwLjk5MTEgMzEuMDIxMiAxMS4wMDI1IDMxLjE1MzQgMTEuMDM5MSAzMS4yNzk1QzExLjA3NTcgMzEuNDA1NyAxMS4xMzY3IDMxLjUyMzQgMTEuMjE4OCAzMS42MjZDMTEuMzAwOSAzMS43Mjg2IDExLjQwMjQgMzEuODE0IDExLjUxNzQgMzEuODc3M0MxMS42MzI1IDMxLjk0MDYgMTEuNzU4OSAzMS45ODA3IDExLjg4OTUgMzEuOTk1MkMxMi4wMjAxIDMyLjAwOTcgMTIuMTUyMiAzMS45OTgzIDEyLjI3ODQgMzEuOTYxN0MxMi40MDQ1IDMxLjkyNTEgMTIuNTIyMiAzMS44NjQxIDEyLjYyNDggMzEuNzgyQzEzLjI5NjEgMzEuMjk3MyAxNC4wOTcyIDMxLjAyNDkgMTQuOTI0OCAzMUgxNC45ODE4QzE4LjU0ODggMzEuMDY1IDIxLjEwODggMzUuNDgyIDIyLjAwNzggNDMuMTE3QzIyLjAzNjcgNDMuMzYwMSAyMi4xNTM4IDQzLjU4NDEgMjIuMzM2OCA0My43NDY2QzIyLjUxOTggNDMuOTA5MSAyMi43NTYgNDMuOTk4OSAyMy4wMDA4IDQzLjk5ODlDMjMuMjQ1NiA0My45OTg5IDIzLjQ4MTggNDMuOTA5MSAyMy42NjQ4IDQzLjc0NjZDMjMuODQ3OCA0My41ODQxIDIzLjk2NDkgNDMuMzYwMSAyMy45OTM4IDQzLjExN0MyNC44OTM4IDM1LjQ4MiAyNy40NTI4IDMxLjA2NSAzMS4wMTk4IDMxSDMxLjA3NjhDMzEuOTA0MyAzMS4wMjgyIDMyLjcwNDcgMzEuMzAxNCAzMy4zNzY4IDMxLjc4NUMzMy41ODQ3IDMxLjk0NTggMzMuODQ3NiAzMi4wMTgzIDM0LjEwODUgMzEuOTg2OEMzNC4zNjk1IDMxLjk1NTIgMzQuNjA3NSAzMS44MjIyIDM0Ljc3MTIgMzEuNjE2NUMzNC45MzQ4IDMxLjQxMDggMzUuMDEwOSAzMS4xNDkgMzQuOTgyOSAzMC44ODc2QzM0Ljk1NSAzMC42MjYzIDM0LjgyNTIgMzAuMzg2NCAzNC42MjE4IDMwLjIyTDM0LjYyNDggMzAuMjE5WiIgZmlsbD0iIzI2MjYzMyIvPjxwYXRoIGQ9Ik0xMC4yNTMxIDQyQzEwLjA5MSA0MS45OTk5IDkuOTMxNDUgNDEuOTYwNCA5Ljc4ODA2IDQxLjg4NUw3LjAwMDA2IDQwLjQyIDQuMjEyMDYgNDEuODg1QzQuMDQ2OTQgNDEuOTcxNSAzLjg2MDkxIDQyLjAxMDIgMy42NzQ5OCA0MS45OTY2IDMuNDg5MDUgNDEuOTgzMSAzLjMxMDYxIDQxLjkxNzggMy4xNTk4MiA0MS44MDgyIDMuMDA5MDIgNDEuNjk4NiAyLjg5MTg3IDQxLjU0OSAyLjgyMTU4IDQxLjM3NjMgMi43NTEzIDQxLjIwMzYgMi43MzA2OCA0MS4wMTQ3IDIuNzYyMDYgNDAuODMxTDMuMjk0MDYgMzcuNzMxIDEuMDM4MDYgMzUuNTMxQy45MDYwODIgMzUuNDAwNS44MTMwMzUgMzUuMjM1OC43NjkzMyAzNS4wNTU0LjcyNTYyNSAzNC44NzUuNzMyOTg2IDM0LjY4Ni43OTA1ODkgMzQuNTA5Ni44NDgxOTIgMzQuMzMzMi45NTM3NjQgMzQuMTc2MiAxLjA5NTQ5IDM0LjA1NjQgMS4yMzcyMSAzMy45MzY1IDEuNDA5NSAzMy44NTg1IDEuNTkzMDYgMzMuODMxTDQuNzA5MDYgMzMuMzc3IDYuMTA5MDYgMzAuNTUzQzYuMjAwNzQgMzAuMzk2OCA2LjMzMTY2IDMwLjI2NzMgNi40ODg4MyAzMC4xNzczIDYuNjQ2IDMwLjA4NzMgNi44MjM5NSAzMC4wNCA3LjAwNTA2IDMwLjA0IDcuMTg2MTYgMzAuMDQgNy4zNjQxMSAzMC4wODczIDcuNTIxMjggMzAuMTc3MyA3LjY3ODQ1IDMwLjI2NzMgNy44MDkzNyAzMC4zOTY4IDcuOTAxMDYgMzAuNTUzTDkuMjk2MDUgMzMuMzc3IDEyLjQxMjEgMzMuODMxQzEyLjU5NTYgMzMuODU4NSAxMi43Njc5IDMzLjkzNjUgMTIuOTA5NiAzNC4wNTY0IDEzLjA1MTMgMzQuMTc2MiAxMy4xNTY5IDM0LjMzMzIgMTMuMjE0NSAzNC41MDk2IDEzLjI3MjEgMzQuNjg2IDEzLjI3OTUgMzQuODc1IDEzLjIzNTggMzUuMDU1NCAxMy4xOTIxIDM1LjIzNTggMTMuMDk5IDM1LjQwMDUgMTIuOTY3MSAzNS41MzFMMTAuNzExMSAzNy43MzEgMTEuMjQzMSA0MC44MzFDMTEuMjY3NyA0MC45NzQ5IDExLjI2MDYgNDEuMTIyNCAxMS4yMjIyIDQxLjI2MzMgMTEuMTgzNyA0MS40MDQxIDExLjExNDkgNDEuNTM0OCAxMS4wMjA2IDQxLjY0NjMgMTAuOTI2MiA0MS43NTc3IDEwLjgwODYgNDEuODQ3MSAxMC42NzYgNDEuOTA4MiAxMC41NDM0IDQxLjk2OTMgMTAuMzk5MSA0Mi4wMDA2IDEwLjI1MzEgNDJ6TTI2LjI1MzEgMTJDMjYuMDkxIDExLjk5OTkgMjUuOTMxNSAxMS45NjA0IDI1Ljc4ODEgMTEuODg1TDIzLjAwMDEgMTAuNDIgMjAuMjEyMSAxMS44ODVDMjAuMDQ2OSAxMS45NzE1IDE5Ljg2MDkgMTIuMDEwMiAxOS42NzUgMTEuOTk2NiAxOS40ODkxIDExLjk4MzEgMTkuMzEwNiAxMS45MTc4IDE5LjE1OTggMTEuODA4MiAxOS4wMDkgMTEuNjk4NiAxOC44OTE5IDExLjU0OSAxOC44MjE2IDExLjM3NjMgMTguNzUxMyAxMS4yMDM2IDE4LjczMDcgMTEuMDE0OCAxOC43NjIxIDEwLjgzMUwxOS4yOTQxIDcuNzMwOTkgMTcuMDM4MSA1LjUzMDk5QzE2LjkwNjEgNS40MDA0OCAxNi44MTMgNS4yMzU4MSAxNi43NjkzIDUuMDU1NDIgMTYuNzI1NiA0Ljg3NTAzIDE2LjczMyA0LjY4NjA0IDE2Ljc5MDYgNC41MDk2IDE2Ljg0ODIgNC4zMzMxNiAxNi45NTM4IDQuMTc2MjMgMTcuMDk1NSA0LjA1NjM4IDE3LjIzNzIgMy45MzY1MyAxNy40MDk1IDMuODU4NDggMTcuNTkzMSAzLjgzMDk5TDIwLjcwOTEgMy4zNzY5OSAyMi4xMDAxLjU0NDk4NkMyMi4xOTE3LjM4ODgwOSAyMi4zMjI3LjI1OTMxMSAyMi40Nzk4LjE2OTMzIDIyLjYzNy4wNzkzNSAyMi44MTUuMDMyMDEyOSAyMi45OTYxLjAzMjAxMjkgMjMuMTc3Mi4wMzIwMTI5IDIzLjM1NTEuMDc5MzUgMjMuNTEyMy4xNjkzMyAyMy42Njk0LjI1OTMxMSAyMy44MDA0LjM4ODgwOSAyMy44OTIxLjU0NDk4NkwyNS4yODcxIDMuMzY4OTkgMjguNDAzMSAzLjgyMjk5QzI4LjU4NjYgMy44NTA0OCAyOC43NTg5IDMuOTI4NTMgMjguOTAwNiA0LjA0ODM4IDI5LjA0MjMgNC4xNjgyMyAyOS4xNDc5IDQuMzI1MTYgMjkuMjA1NSA0LjUwMTYgMjkuMjYzMSA0LjY3ODA0IDI5LjI3MDUgNC44NjcwMyAyOS4yMjY4IDUuMDQ3NDIgMjkuMTgzMSA1LjIyNzgxIDI5LjA5IDUuMzkyNDcgMjguOTU4MSA1LjUyMjk5TDI2LjcwMjEgNy43MjI5OSAyNy4yMzQxIDEwLjgyM0MyNy4yNTk5IDEwLjk2NjcgMjcuMjU0IDExLjExNDQgMjcuMjE2NiAxMS4yNTU2IDI3LjE3OTMgMTEuMzk2OCAyNy4xMTE1IDExLjUyODEgMjcuMDE4IDExLjY0MDIgMjYuOTI0NSAxMS43NTI0IDI2LjgwNzYgMTEuODQyOCAyNi42NzU0IDExLjkwNDkgMjYuNTQzMyAxMS45NjcxIDI2LjM5OTEgMTEuOTk5NSAyNi4yNTMxIDEyek00Mi4yNTMxIDQyQzQyLjA5MSA0MS45OTk5IDQxLjkzMTQgNDEuOTYwNCA0MS43ODgxIDQxLjg4NUwzOS4wMDAxIDQwLjQyIDM2LjIxMjEgNDEuODg1QzM2LjA0NjkgNDEuOTcxNSAzNS44NjA5IDQyLjAxMDIgMzUuNjc1IDQxLjk5NjYgMzUuNDg5IDQxLjk4MzEgMzUuMzEwNiA0MS45MTc4IDM1LjE1OTggNDEuODA4MiAzNS4wMDkgNDEuNjk4NiAzNC44OTE5IDQxLjU0OSAzNC44MjE2IDQxLjM3NjMgMzQuNzUxMyA0MS4yMDM2IDM0LjczMDcgNDEuMDE0OCAzNC43NjIxIDQwLjgzMUwzNS4yOTQxIDM3LjczMSAzMy4wMzgxIDM1LjUzMUMzMi45MDYxIDM1LjQwMDUgMzIuODEzIDM1LjIzNTggMzIuNzY5MyAzNS4wNTU0IDMyLjcyNTYgMzQuODc1IDMyLjczMyAzNC42ODYgMzIuNzkwNiAzNC41MDk2IDMyLjg0ODIgMzQuMzMzMiAzMi45NTM4IDM0LjE3NjIgMzMuMDk1NSAzNC4wNTY0IDMzLjIzNzIgMzMuOTM2NSAzMy40MDk1IDMzLjg1ODUgMzMuNTkzMSAzMy44MzFMMzYuNzA5MSAzMy4zNzcgMzguMTAwMSAzMC41NDVDMzguMTkxNyAzMC4zODg4IDM4LjMyMjcgMzAuMjU5MyAzOC40Nzk4IDMwLjE2OTMgMzguNjM3IDMwLjA3OTQgMzguODE0OSAzMC4wMzIgMzguOTk2MSAzMC4wMzIgMzkuMTc3MiAzMC4wMzIgMzkuMzU1MSAzMC4wNzk0IDM5LjUxMjMgMzAuMTY5MyAzOS42Njk0IDMwLjI1OTMgMzkuODAwNCAzMC4zODg4IDM5Ljg5MjEgMzAuNTQ1TDQxLjI4NzEgMzMuMzY5IDQ0LjQwMzEgMzMuODIzQzQ0LjU4NjYgMzMuODUwNSA0NC43NTg5IDMzLjkyODUgNDQuOTAwNiAzNC4wNDg0IDQ1LjA0MjMgMzQuMTY4MiA0NS4xNDc5IDM0LjMyNTIgNDUuMjA1NSAzNC41MDE2IDQ1LjI2MzEgMzQuNjc4IDQ1LjI3MDUgMzQuODY3IDQ1LjIyNjggMzUuMDQ3NCA0NS4xODMxIDM1LjIyNzggNDUuMDkgMzUuMzkyNSA0NC45NTgxIDM1LjUyM0w0Mi43MDIxIDM3LjcyMyA0My4yMzQxIDQwLjgyM0M0My4yNTk5IDQwLjk2NjcgNDMuMjU0IDQxLjExNDQgNDMuMjE2NiA0MS4yNTU2IDQzLjE3OTMgNDEuMzk2OCA0My4xMTE1IDQxLjUyODEgNDMuMDE4IDQxLjY0MDIgNDIuOTI0NSA0MS43NTI0IDQyLjgwNzYgNDEuODQyOCA0Mi42NzU0IDQxLjkwNDkgNDIuNTQzMyA0MS45NjcxIDQyLjM5OTEgNDEuOTk5NSA0Mi4yNTMxIDQyeiIgZmlsbD0iIzE2QjM3OCIvPjxwYXRoIGQ9Ik05IDE0QzguMjA0MzUgMTQgNy40NDEyOSAxMy42ODM5IDYuODc4NjggMTMuMTIxMyA2LjMxNjA3IDEyLjU1ODcgNiAxMS43OTU2IDYgMTEgNiAxMC43MzQ4IDUuODk0NjQgMTAuNDgwNCA1LjcwNzExIDEwLjI5MjkgNS41MTk1NyAxMC4xMDU0IDUuMjY1MjIgMTAgNSAxMCA0LjczNDc4IDEwIDQuNDgwNDMgMTAuMTA1NCA0LjI5Mjg5IDEwLjI5MjkgNC4xMDUzNiAxMC40ODA0IDQgMTAuNzM0OCA0IDExIDQgMTEuNzk1NiAzLjY4MzkzIDEyLjU1ODcgMy4xMjEzMiAxMy4xMjEzIDIuNTU4NzEgMTMuNjgzOSAxLjc5NTY1IDE0IDEgMTQgLjczNDc4NCAxNCAuNDgwNDMgMTQuMTA1NC4yOTI4OTMgMTQuMjkyOS4xMDUzNTcgMTQuNDgwNCAwIDE0LjczNDggMCAxNSAwIDE1LjI2NTIuMTA1MzU3IDE1LjUxOTYuMjkyODkzIDE1LjcwNzEuNDgwNDMgMTUuODk0Ni43MzQ3ODQgMTYgMSAxNiAxLjc5NTY1IDE2IDIuNTU4NzEgMTYuMzE2MSAzLjEyMTMyIDE2Ljg3ODcgMy42ODM5MyAxNy40NDEzIDQgMTguMjA0NCA0IDE5IDQgMTkuMjY1MiA0LjEwNTM2IDE5LjUxOTYgNC4yOTI4OSAxOS43MDcxIDQuNDgwNDMgMTkuODk0NiA0LjczNDc4IDIwIDUgMjAgNS4yNjUyMiAyMCA1LjUxOTU3IDE5Ljg5NDYgNS43MDcxMSAxOS43MDcxIDUuODk0NjQgMTkuNTE5NiA2IDE5LjI2NTIgNiAxOSA2IDE4LjIwNDQgNi4zMTYwNyAxNy40NDEzIDYuODc4NjggMTYuODc4NyA3LjQ0MTI5IDE2LjMxNjEgOC4yMDQzNSAxNiA5IDE2IDkuMjY1MjIgMTYgOS41MTk1NyAxNS44OTQ2IDkuNzA3MTEgMTUuNzA3MSA5Ljg5NDY0IDE1LjUxOTYgMTAgMTUuMjY1MiAxMCAxNSAxMCAxNC43MzQ4IDkuODk0NjQgMTQuNDgwNCA5LjcwNzExIDE0LjI5MjkgOS41MTk1NyAxNC4xMDU0IDkuMjY1MjIgMTQgOSAxNHpNNDUgMTMuOTg3QzQ0LjIwNDQgMTMuOTg3IDQzLjQ0MTMgMTMuNjcwOSA0Mi44Nzg3IDEzLjEwODMgNDIuMzE2MSAxMi41NDU3IDQyIDExLjc4MjYgNDIgMTAuOTg3IDQyIDEwLjcyMTggNDEuODk0NiAxMC40Njc0IDQxLjcwNzEgMTAuMjc5OSA0MS41MTk2IDEwLjA5MjQgNDEuMjY1MiA5Ljk4NyA0MSA5Ljk4NyA0MC43MzQ4IDkuOTg3IDQwLjQ4MDQgMTAuMDkyNCA0MC4yOTI5IDEwLjI3OTkgNDAuMTA1NCAxMC40Njc0IDQwIDEwLjcyMTggNDAgMTAuOTg3IDQwIDExLjc4MjYgMzkuNjgzOSAxMi41NDU3IDM5LjEyMTMgMTMuMTA4MyAzOC41NTg3IDEzLjY3MDkgMzcuNzk1NiAxMy45ODcgMzcgMTMuOTg3IDM2LjczNDggMTMuOTg3IDM2LjQ4MDQgMTQuMDkyNCAzNi4yOTI5IDE0LjI3OTkgMzYuMTA1NCAxNC40Njc0IDM2IDE0LjcyMTggMzYgMTQuOTg3IDM2IDE1LjI1MjIgMzYuMTA1NCAxNS41MDY2IDM2LjI5MjkgMTUuNjk0MSAzNi40ODA0IDE1Ljg4MTYgMzYuNzM0OCAxNS45ODcgMzcgMTUuOTg3IDM3Ljc5NTYgMTUuOTg3IDM4LjU1ODcgMTYuMzAzMSAzOS4xMjEzIDE2Ljg2NTcgMzkuNjgzOSAxNy40MjgzIDQwIDE4LjE5MTMgNDAgMTguOTg3IDQwIDE5LjI1MjIgNDAuMTA1NCAxOS41MDY2IDQwLjI5MjkgMTkuNjk0MSA0MC40ODA0IDE5Ljg4MTYgNDAuNzM0OCAxOS45ODcgNDEgMTkuOTg3IDQxLjI2NTIgMTkuOTg3IDQxLjUxOTYgMTkuODgxNiA0MS43MDcxIDE5LjY5NDEgNDEuODk0NiAxOS41MDY2IDQyIDE5LjI1MjIgNDIgMTguOTg3IDQyIDE4LjE5MTMgNDIuMzE2MSAxNy40MjgzIDQyLjg3ODcgMTYuODY1NyA0My40NDEzIDE2LjMwMzEgNDQuMjA0NCAxNS45ODcgNDUgMTUuOTg3IDQ1LjI2NTIgMTUuOTg3IDQ1LjUxOTYgMTUuODgxNiA0NS43MDcxIDE1LjY5NDEgNDUuODk0NiAxNS41MDY2IDQ2IDE1LjI1MjIgNDYgMTQuOTg3IDQ2IDE0LjcyMTggNDUuODk0NiAxNC40Njc0IDQ1LjcwNzEgMTQuMjc5OSA0NS41MTk2IDE0LjA5MjQgNDUuMjY1MiAxMy45ODcgNDUgMTMuOTg3eiIgZmlsbD0iI0U2QTExNyIvPjxwYXRoIGQ9Ik0xOS4wMDAyIDI4QzE4LjgxMzYgMjguMDAxIDE4LjYzMDMgMjcuOTQ5NyAxOC40NzEzIDI3Ljg1MiAxOC4zMTIyIDI3Ljc1NDMgMTguMTgzNyAyNy42MTQgMTguMTAwMiAyNy40NDcgMTQuNDUzMiAyMC4xNTMgOS4yMDgyMyAxOS45OSA4Ljk4NjIzIDE5Ljk4NyA4Ljg1NDkxIDE5Ljk4NjQgOC43MjQ5OSAxOS45NTk5IDguNjAzODkgMTkuOTA5MSA4LjQ4Mjc5IDE5Ljg1ODMgOC4zNzI4OCAxOS43ODQyIDguMjgwNDQgMTkuNjkwOSA4LjA5Mzc1IDE5LjUwMjUgNy45ODk1NCAxOS4yNDc3IDcuOTkwNzMgMTguOTgyNSA3Ljk5MTkzIDE4LjcxNzMgOC4wOTg0MyAxOC40NjM0IDguMjg2ODEgMTguMjc2NyA4LjQ3NTE5IDE4LjA5IDguNzMwMDIgMTcuOTg1OCA4Ljk5NTIzIDE3Ljk4NyA5LjI2NTIzIDE3Ljk4NyAxNS42NjAyIDE4LjA5OSAxOS44ODgyIDI2LjU1MyAxOS45NjQyIDI2LjcwNDkgMjAuMDAwMSAyNi44NzM4IDE5Ljk5MjggMjcuMDQzNSAxOS45ODU0IDI3LjIxMzIgMTkuOTM0OSAyNy4zNzgzIDE5Ljg0NiAyNy41MjMgMTkuNzU3MiAyNy42Njc4IDE5LjYzMjkgMjcuNzg3NiAxOS40ODQ5IDI3Ljg3MTEgMTkuMzM2OSAyNy45NTQ1IDE5LjE3MDEgMjcuOTk4OSAxOS4wMDAyIDI4ek0yNyAyOEMyNi44Mjk2IDI3Ljk5OTkgMjYuNjYyIDI3Ljk1NjMgMjYuNTEzMiAyNy44NzMzIDI2LjM2NDQgMjcuNzkwMiAyNi4yMzkzIDI3LjY3MDYgMjYuMTQ5OCAyNy41MjU2IDI2LjA2MDIgMjcuMzgwNiAyNi4wMDkyIDI3LjIxNTIgMjYuMDAxNSAyNy4wNDUgMjUuOTkzOCAyNi44NzQ4IDI2LjAyOTggMjYuNzA1NCAyNi4xMDYgMjYuNTUzIDMwLjMzMyAxOC4xIDM2LjcyOCAxNy45ODggMzcgMTcuOTg3IDM3LjI2NTIgMTcuOTg3IDM3LjUxOTUgMTguMDkyNCAzNy43MDcxIDE4LjI3OTkgMzcuODk0NiAxOC40Njc0IDM4IDE4LjcyMTggMzggMTguOTg3IDM4IDE5LjI1MjIgMzcuODk0NiAxOS41MDY2IDM3LjcwNzEgMTkuNjk0MSAzNy41MTk1IDE5Ljg4MTYgMzcuMjY1MiAxOS45ODcgMzcgMTkuOTg3IDM2Ljc4NSAxOS45ODcgMzEuNTQgMjAuMTUzIDI3Ljg5MyAyNy40NDcgMjcuODEgMjcuNjEyOSAyNy42ODI2IDI3Ljc1MjUgMjcuNTI0OSAyNy44NTAyIDI3LjM2NzIgMjcuOTQ3OCAyNy4xODU0IDI3Ljk5OTcgMjcgMjh6IiBmaWxsPSIjMjYyNjMzIi8+PC9zdmc+'); + --icon-Star: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjUiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yLjI0MDgxIDQuODQ5MjNDMi42MDAyMyA0LjQyOTkgMy4yMzE1MyA0LjM4MTM0IDMuNjUwODYgNC43NDA3Nkw3LjE1MDg2IDcuNzQwNzZDNy41NzAxOCA4LjEwMDE4IDcuNjE4NzUgOC43MzE0OCA3LjI1OTMyIDkuMTUwODFDNi44OTk5IDkuNTcwMTQgNi4yNjg2IDkuNjE4NyA1Ljg0OTI4IDkuMjU5MjdMMi4zNDkyOCA2LjI1OTI3QzEuOTI5OTUgNS44OTk4NSAxLjg4MTM5IDUuMjY4NTUgMi4yNDA4MSA0Ljg0OTIzWk0xMS41IDcuMDIwODFDMTEuMTM3IDcuMDIwODEgMTAuODA1MiA3LjIyNTkgMTAuNjQyOSA3LjU1MDU3TDguODc0NTUgMTEuMDg3Mkw0Ljk4NzA5IDExLjY1MjdDNC42MjYxNiAxMS43MDUyIDQuMzI2MzEgMTEuOTU4IDQuMjEzNiAxMi4zMDQ5QzQuMTAwOSAxMi42NTE4IDQuMTk0ODcgMTMuMDMyNiA0LjQ1NjAyIDEzLjI4NzJMNy4yODYxOSAxNi4wNDY2TDYuNjQ5NTkgMTkuOTM2OUM2LjU5MDc5IDIwLjI5NjIgNi43NDA3MSAyMC42NTc4IDcuMDM2NTEgMjAuODcwMkM3LjMzMjMxIDIxLjA4MjUgNy43MjI4OSAyMS4xMDg5IDguMDQ0NTMgMjAuOTM4MkwxMS41IDE5LjEwNDdMMTQuOTU1NSAyMC45MzgyQzE1LjI3NzIgMjEuMTA4OSAxNS42Njc4IDIxLjA4MjUgMTUuOTYzNiAyMC44NzAyQzE2LjI1OTQgMjAuNjU3OCAxNi40MDkzIDIwLjI5NjIgMTYuMzUwNSAxOS45MzY5TDE1LjcxMzkgMTYuMDQ2NkwxOC41NDQgMTMuMjg3MkMxOC44MDUyIDEzLjAzMjYgMTguODk5MiAxMi42NTE4IDE4Ljc4NjUgMTIuMzA0OUMxOC42NzM4IDExLjk1OCAxOC4zNzM5IDExLjcwNTIgMTguMDEzIDExLjY1MjdMMTQuMTI1NSAxMS4wODcyTDEyLjM1NzIgNy41NTA1N0MxMi4xOTQ5IDcuMjI1OSAxMS44NjMgNy4wMjA4MSAxMS41IDcuMDIwODFaTTEwLjM2NSAxMi4zOTIxTDExLjUgMTAuMTIyTDEyLjYzNTEgMTIuMzkyMUMxMi43NzUzIDEyLjY3MjYgMTMuMDQzOSAxMi44NjY3IDEzLjM1NDMgMTIuOTExOUwxNS44MTYyIDEzLjI3TDE0LjAxODUgMTUuMDIyN0MxMy43OTQ2IDE1LjI0MSAxMy42OTEzIDE1LjU1NDkgMTMuNzQxOCAxNS44NjM2TDE0LjE0NjkgMTguMzM5M0wxMS45NDkyIDE3LjE3MzJDMTEuNjY4MyAxNy4wMjQyIDExLjMzMTggMTcuMDI0MiAxMS4wNTA4IDE3LjE3MzJMOC44NTMxNyAxOC4zMzkzTDkuMjU4MjkgMTUuODYzNkM5LjMwODggMTUuNTU0OSA5LjIwNTUgMTUuMjQxIDguOTgxNTUgMTUuMDIyN0w3LjE4MzkxIDEzLjI3TDkuNjQ1NzkgMTIuOTExOUM5Ljk1NjEzIDEyLjg2NjcgMTAuMjI0OCAxMi42NzI2IDEwLjM2NSAxMi4zOTIxWk0xOS4zNDkyIDQuNzQwNzZDMTkuNzY4NiA0LjM4MTM0IDIwLjM5OTkgNC40Mjk5IDIwLjc1OTMgNC44NDkyM0MyMS4xMTg3IDUuMjY4NTUgMjEuMDcwMSA1Ljg5OTg1IDIwLjY1MDggNi4yNTkyN0wxNy4xNTA4IDkuMjU5MjdDMTYuNzMxNSA5LjYxODcgMTYuMTAwMiA5LjU3MDE0IDE1Ljc0MDggOS4xNTA4MUMxNS4zODE0IDguNzMxNDggMTUuNDI5OSA4LjEwMDE4IDE1Ljg0OTIgNy43NDA3NkwxOS4zNDkyIDQuNzQwNzZaTTIyLjI3NTMgMTkuNjMxNkMyMS45MjY1IDIwLjA1OTggMjEuMjk2NiAyMC4xMjQxIDIwLjg2ODQgMTkuNzc1M0wxOC4zNjg0IDE3LjczODZDMTcuOTQwMyAxNy4zODk3IDE3Ljg3NiAxNi43NTk4IDE4LjIyNDggMTYuMzMxN0MxOC41NzM2IDE1LjkwMzUgMTkuMjAzNSAxNS44MzkyIDE5LjYzMTcgMTYuMTg4TDIyLjEzMTcgMTguMjI0N0MyMi41NTk5IDE4LjU3MzYgMjIuNjI0MiAxOS4yMDM1IDIyLjI3NTMgMTkuNjMxNlpNMi4xMzE2NSAxOS43NzUzQzEuNzAzNDcgMjAuMTI0MSAxLjA3MzU4IDIwLjA1OTggMC43MjQ3NTIgMTkuNjMxNkMwLjM3NTkxOSAxOS4yMDM1IDAuNDQwMjM4IDE4LjU3MzYgMC44Njg0MTMgMTguMjI0N0wzLjM2ODQxIDE2LjE4OEMzLjc5NjU5IDE1LjgzOTIgNC40MjY0OCAxNS45MDM1IDQuNzc1MzEgMTYuMzMxN0M1LjEyNDE0IDE2Ljc1OTggNS4wNTk4MiAxNy4zODk3IDQuNjMxNjUgMTcuNzM4NkwyLjEzMTY1IDE5Ljc3NTNaIiBmaWxsPSIjMTZCMzc4Ii8+PC9zdmc+'); --icon-Tick: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjYxODMwNjksNC42NzcwMjg0NyBDMTEuNzk2Njc4OSw0LjQ2NjIyNTE3IDEyLjExMjE2NzgsNC40Mzk5MzQ0MyAxMi4zMjI5NzExLDQuNjE4MzA2NDUgQzEyLjUzMzc3NDQsNC43OTY2Nzg0OCAxMi41NjAwNjUyLDUuMTEyMTY3NDEgMTIuMzgxNjkzMSw1LjMyMjk3MDcxIEw2LjUzMDY4ODI3LDEyLjIzNzc5NDYgTDMuNjQ2NDQ2NjEsOS4zNTM1NTI5OCBDMy40NTExODQ0Niw5LjE1ODI5MDg0IDMuNDUxMTg0NDYsOC44NDE3MDgzNSAzLjY0NjQ0NjYxLDguNjQ2NDQ2MiBDMy44NDE3MDg3Niw4LjQ1MTE4NDA2IDQuMTU4MjkxMjQsOC40NTExODQwNiA0LjM1MzU1MzM5LDguNjQ2NDQ2MiBMNi40NjkzMTE3MywxMC43NjIyMDQ1IEwxMS42MTgzMDY5LDQuNjc3MDI4NDcgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-TickSolid: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0ibSA2MS40NCwwIGMgMzMuOTMzLDAgNjEuNDQxLDI3LjUwNyA2MS40NDEsNjEuNDM5IDAsMzMuOTMzIC0yNy41MDgsNjEuNDQgLTYxLjQ0MSw2MS40NCBDIDI3LjUwOCwxMjIuODggMCw5NS4zNzIgMCw2MS40MzkgMCwyNy41MDcgMjcuNTA4LDAgNjEuNDQsMCBaIE0gMzQuMTA2LDY3LjY3OCAzNC4wOTEsNjcuNjY0IGMgLTAuNzg1LC0wLjcxOCAtMS4yMDcsLTEuNjg1IC0xLjI1NiwtMi42NjkgLTAuMDQ5LC0wLjk4MiAwLjI3NSwtMS45ODUgMC45ODQsLTIuNzc3IDAuMDEsLTAuMDExIDAuMDE5LC0wLjAyMSAwLjAyOSwtMC4wMzEgMC43MTcsLTAuNzg0IDEuNjg0LC0xLjIwNyAyLjY2OCwtMS4yNTYgMC45ODksLTAuMDQ5IDEuOTk4LDAuMjggMi43OTIsMC45OTggTCA1Mi4yNjQsNzMuNjc3IDgzLjM1Myw0MS4xMTggdiAwIGMgMC43NCwtMC43NzYgMS43MjMsLTEuMTggMi43MTksLTEuMjA0IDAuOTkyLC0wLjAyNSAxLjk5NCwwLjMyOSAyLjc3MSwxLjA2NyB2IDEwZS00IGMgMC43NzcsMC43MzkgMS4xOCwxLjcyNCAxLjIwNSwyLjcxOCAwLjAyNSwwLjk5MyAtMC4zMywxLjk5NyAtMS4wNjgsMi43NzMgTCA1NS4yNzksODEuNzY5IGMgLTAuMDIzLDAuMDI0IC0wLjA0OCwwLjA0NyAtMC4wNzMsMC4wNjcgLTAuNzE1LDAuNzE1IC0xLjY0OSwxLjA5NSAtMi41OTgsMS4xMyAtMC45NzQsMC4wMzcgLTEuOTYzLC0wLjI5MyAtMi43NDQsLTEgTCAzNC4xMTgsNjcuNjg4IFoiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09Im1hdHJpeCguMTMwMiAwIDAgLjEzMDIgMCAwKSIvPjwvc3ZnPg=='); --icon-Undo: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIuMzU1MjI1NjIsNSBMNi41LDUgQzYuNzc2MTQyMzcsNSA3LDUuMjIzODU3NjMgNyw1LjUgQzcsNS43NzYxNDIzNyA2Ljc3NjE0MjM3LDYgNi41LDYgTDEuNTEwMTA5OTksNiBDMS40ODY3NTIwNiw2LjAwMDQ5MTk4IDEuNDYzMjU2NTksNS45OTkzNDQxNCAxLjQzOTc5Mzg5LDUuOTk2NDk5MjUgQzEuMzYwMjgyMTYsNS45ODY4NzA1OCAxLjI4NjUwNTA1LDUuOTU4NjY1NjMgMS4yMjI5NzM2NSw1LjkxNjMwNDg0IEMxLjA5OTIxNTYzLDUuODM0MTc1NDcgMS4wMjE4NjQ3MSw1LjcwMzQ3ODggMS4wMDM5MTQxNyw1LjU2Mjg2NjI5IEMxLjAwMTgxMDM5LDUuNTQ2MDk2MDIgMS4wMDA1MzcxOCw1LjUyOTA2NzQgMS4wMDAxMzcwNCw1LjUxMTgyMjkyIEMwLjk5OTg1Mzk4Nyw1LjUwNDI3MzIxIDAuOTk5ODQwMjk5LDUuNDk2NzA0NiAxLDUuNDg5MTI2OTMgTDEsMC41IEMxLDAuMjIzODU3NjI1IDEuMjIzODU3NjMsMCAxLjUsMCBDMS43NzYxNDIzNywwIDIsMC4yMjM4NTc2MjUgMiwwLjUgTDIsMy43NzQ2OTMxNiBDMy4zNjg1NDA5LDIuMDM5NDAzNDMgNS4zMjkwNzU2LDEgNy41LDEgQzExLjY0MjE0MjQsMSAxNSw0LjM1Nzg1NzYzIDE1LDguNSBDMTUsMTIuNjQyMTQyNCAxMS42NDIxNDI0LDE2IDcuNSwxNiBDNy4yMjM4NTc2MywxNiA3LDE1Ljc3NjE0MjQgNywxNS41IEM3LDE1LjIyMzg1NzYgNy4yMjM4NTc2MywxNSA3LjUsMTUgQzExLjA4OTg1NzYsMTUgMTQsMTIuMDg5ODU3NiAxNCw4LjUgQzE0LDQuOTEwMTQyMzcgMTEuMDg5ODU3NiwyIDcuNSwyIEM1LjQxNTc0NjU0LDIgMy41NDU1NTE2MywzLjEzMDY0MDcgMi4zNTUyMjU2Miw1IFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg=='); --icon-Validation: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE1LDEwIEwxMSwxMCBMMTEsMTIuNSBDMTEsMTIuNzc2MTQyNCAxMC43NzYxNDI0LDEzIDEwLjUsMTMgTDUuNSwxMyBDNS4yMjM4NTc2MywxMyA1LDEyLjc3NjE0MjQgNSwxMi41IEw1LDEwIEwxLDEwIEwxLDE0LjUgQzEsMTQuNzc1ODU3NiAxLjIyNDE0MjM3LDE1IDEuNSwxNSBMMTQuNSwxNSBDMTQuNzc1ODU3NiwxNSAxNSwxNC43NzU4NTc2IDE1LDE0LjUgTDE1LDEwIFogTTE1LDkgTDE1LDEuNSBDMTUsMS4yMjQxNDIzNyAxNC43NzU4NTc2LDEgMTQuNSwxIEwxLjUsMSBDMS4yMjQxNDIzNywxIDEsMS4yMjQxNDIzNyAxLDEuNSBMMSw5IEw1LjUsOSBDNS43NzYxNDIzNyw5IDYsOS4yMjM4NTc2MyA2LDkuNSBMNiwxMiBMMTAsMTIgTDEwLDkuNSBDMTAsOS4yMjM4NTc2MyAxMC4yMjM4NTc2LDkgMTAuNSw5IEwxNSw5IFogTTE0LjUsMTYgTDEuNSwxNiBDMC42NzE4NTc2MjUsMTYgMCwxNS4zMjgxNDI0IDAsMTQuNSBMMCwxLjUgQzAsMC42NzE4NTc2MjUgMC42NzE4NTc2MjUsMCAxLjUsMCBMMTQuNSwwIEMxNS4zMjgxNDI0LDAgMTYsMC42NzE4NTc2MjUgMTYsMS41IEwxNiwxNC41IEMxNiwxNS4zMjgxNDI0IDE1LjMyODE0MjQsMTYgMTQuNSwxNiBaIE0xMS4xNDY0NDY2LDMuMTQ2NDQ2NjEgQzExLjM0MTcwODgsMi45NTExODQ0NiAxMS42NTgyOTEyLDIuOTUxMTg0NDYgMTEuODUzNTUzNCwzLjE0NjQ0NjYxIEMxMi4wNDg4MTU1LDMuMzQxNzA4NzYgMTIuMDQ4ODE1NSwzLjY1ODI5MTI0IDExLjg1MzU1MzQsMy44NTM1NTMzOSBMNy44NTM1NTMzOSw3Ljg1MzU1MzM5IEM3LjY1ODI5MTI0LDguMDQ4ODE1NTQgNy4zNDE3MDg3Niw4LjA0ODgxNTU0IDcuMTQ2NDQ2NjEsNy44NTM1NTMzOSBMNS4xNDY0NDY2MSw1Ljg1MzU1MzM5IEM0Ljk1MTE4NDQ2LDUuNjU4MjkxMjQgNC45NTExODQ0Niw1LjM0MTcwODc2IDUuMTQ2NDQ2NjEsNS4xNDY0NDY2MSBDNS4zNDE3MDg3Niw0Ljk1MTE4NDQ2IDUuNjU4MjkxMjQsNC45NTExODQ0NiA1Ljg1MzU1MzM5LDUuMTQ2NDQ2NjEgTDcuNSw2Ljc5Mjg5MzIyIEwxMS4xNDY0NDY2LDMuMTQ2NDQ2NjEgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-Video: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE0LjUsMSBDMTUuMzI4NDI3MSwxIDE2LDEuNjcxNTcyODggMTYsMi41IEwxNiwyLjUgTDE2LDEzLjUgQzE2LDE0LjMyODQyNzEgMTUuMzI4NDI3MSwxNSAxNC41LDE1IEwxNC41LDE1IEwxLjUsMTUgQzAuNjcxNTcyODc1LDE1IDAsMTQuMzI4NDI3MSAwLDEzLjUgTDAsMTMuNSBMMCwyLjUgQzAsMS42NzE1NzI4OCAwLjY3MTU3Mjg3NSwxIDEuNSwxIEwxLjUsMSBaIE0xNC41LDIgTDEuNSwyIEMxLjIyMzg1NzYzLDIgMSwyLjIyMzg1NzYzIDEsMi41IEwxLDIuNSBMMSwxMy41IEMxLDEzLjc3NjE0MjQgMS4yMjM4NTc2MywxNCAxLjUsMTQgTDEuNSwxNCBMMTQuNSwxNCBDMTQuNzc2MTQyNCwxNCAxNSwxMy43NzYxNDI0IDE1LDEzLjUgTDE1LDEzLjUgTDE1LDIuNSBDMTUsMi4yMjM4NTc2MyAxNC43NzYxNDI0LDIgMTQuNSwyIEwxNC41LDIgWiBNNSw0LjUgQzUsNC4xMDU0NTQ4NiA1LjQzNTU3NTIxLDMuODY2MzQ3NjQgNS43Njg0Mzc3NSw0LjA3ODE2OTI2IEw1Ljc2ODQzNzc1LDQuMDc4MTY5MjYgTDExLjI2ODQzNzcsNy41NzgxNjkyNiBDMTEuNTc3MTg3NCw3Ljc3NDY0NjMyIDExLjU3NzE4NzQsOC4yMjUzNTM2OCAxMS4yNjg0Mzc3LDguNDIxODMwNzQgTDExLjI2ODQzNzcsOC40MjE4MzA3NCBMNS43Njg0Mzc3NSwxMS45MjE4MzA3IEM1LjQzNTU3NTIxLDEyLjEzMzY1MjQgNSwxMS44OTQ1NDUxIDUsMTEuNSBMNSwxMS41IFogTTYsNS40MSBMNiwxMC41ODkgTDEwLjA2OSw4IEw2LDUuNDEgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); + --icon-VideoPlay: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzkiIGhlaWdodD0iMzUiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik00LjA2MjUgMy4yNUMzLjQwNjY2IDMuMjUgMi44NzUgMy43ODE2NiAyLjg3NSA0LjQzNzVWMzAuNTYyNUMyLjg3NSAzMS4yMTgzIDMuNDA2NjYgMzEuNzUgNC4wNjI1IDMxLjc1SDM0LjkzNzVDMzUuNTkzMyAzMS43NSAzNi4xMjUgMzEuMjE4MyAzNi4xMjUgMzAuNTYyNVY0LjQzNzVDMzYuMTI1IDMuNzgxNjYgMzUuNTkzMyAzLjI1IDM0LjkzNzUgMy4yNUg0LjA2MjVaTTAuNSA0LjQzNzVDMC41IDIuNDY5OTkgMi4wOTQ5OSAwLjg3NSA0LjA2MjUgMC44NzVIMzQuOTM3NUMzNi45MDUgMC44NzUgMzguNSAyLjQ2OTk5IDM4LjUgNC40Mzc1VjMwLjU2MjVDMzguNSAzMi41MyAzNi45MDUgMzQuMTI1IDM0LjkzNzUgMzQuMTI1SDQuMDYyNUMyLjA5NDk5IDM0LjEyNSAwLjUgMzIuNTMgMC41IDMwLjU2MjVWNC40Mzc1WiIvPjxwYXRoIGQ9Ik0xMi45OTExIDguMTQ2NTNDMTMuMzcxIDcuOTM3OTcgMTMuODM0NCA3Ljk1Mjk2IDE0LjIgOC4xODU2NUwyNy4yNjI1IDE2LjQ5ODJDMjcuNjA1MSAxNi43MTYxIDI3LjgxMjUgMTcuMDk0IDI3LjgxMjUgMTcuNUMyNy44MTI1IDE3LjkwNiAyNy42MDUxIDE4LjI4MzkgMjcuMjYyNSAxOC41MDE5TDE0LjIgMjYuODE0NEMxMy44MzQ0IDI3LjA0NyAxMy4zNzEgMjcuMDYyIDEyLjk5MTEgMjYuODUzNUMxMi42MTExIDI2LjY0NDkgMTIuMzc1IDI2LjI0NTkgMTIuMzc1IDI1LjgxMjVWOS4xODc1QzEyLjM3NSA4Ljc1NDA5IDEyLjYxMTEgOC4zNTUwOSAxMi45OTExIDguMTQ2NTNaTTE0Ljc1IDExLjM1MDdWMjMuNjQ5M0wyNC40MTMxIDE3LjVMMTQuNzUgMTEuMzUwN1oiLz48L2c+PC9zdmc+'); + --icon-VideoPlay2: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUiIGhlaWdodD0iMjUiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYuNSA3LjI0MTA1QzYuNSA1LjY5NzQ3IDguMTc0NDMgNC43MzU3MyA5LjUwNzc0IDUuNTEzNDlMMTguNTIzMSAxMC43NzI1QzE5Ljg0NjEgMTEuNTQ0MiAxOS44NDYxIDEzLjQ1NTggMTguNTIzMSAxNC4yMjc2TDkuNTA3NzQgMTkuNDg2NUM4LjE3NDQzIDIwLjI2NDMgNi41IDE5LjMwMjYgNi41IDE3Ljc1OVY3LjI0MTA1Wk0xNy41MTU0IDEyLjVMOC41IDcuMjQxMDVWMTcuNzU5TDE3LjUxNTQgMTIuNVoiIGZpbGw9IiNmZmYiLz48L3N2Zz4='); --icon-Warning: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PHBhdGggZD0ibSA2NS40NSwxLjk3MiA1NS41OTQsODcuMzIzIGMgMS42NzMsMi42MyAzLjExNyw4LjAxNiAwLDguMDE2IEggMS44MzcgYyAtMy4xMTgsMCAtMS42NzYsLTUuMzg2IDAsLTguMDE2IEwgNTcuNDMxLDEuOTcyIGMgMS42NzUsLTIuNjMgNi4zNDMsLTIuNjI4IDguMDE5LDAgeiBtIC04Ljg3Miw3Mi4xNzQgaCA5LjY4MiB2IDguNTYxIGggLTkuNjgyIHogbSA5LjY3NiwtNS45MjkgSCA1Ni41OCBDIDU1LjYxNiw1Ni40NjEgNTMuNTk4LDQ5LjAwMSA1My41OTgsMzcuMjYyIGMgMCwtNC4zMzEgMy41MSwtNy44NDIgNy44NDEsLTcuODQyIDQuMzMyLDAgNy44NDIsMy41MTEgNy44NDIsNy44NDIgMTBlLTQsMTEuNzM0IC0yLjA0NSwxOS4yMDkgLTMuMDI3LDMwLjk1NSB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIgdHJhbnNmb3JtPSJtYXRyaXgoLjEzMDIgMCAwIC4xMzAyIDAgMS42NjUpIi8+PC9zdmc+'); --icon-Widget: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEuNSwxIEMxLjIyMzg1NzYzLDEgMSwxLjIyMzg1NzYzIDEsMS41IEwxLDUuNSBDMSw1Ljc3NjE0MjM3IDEuMjIzODU3NjMsNiAxLjUsNiBMNS41LDYgQzUuNzc2MTQyMzcsNiA2LDUuNzc2MTQyMzcgNiw1LjUgTDYsMS41IEM2LDEuMjIzODU3NjMgNS43NzYxNDIzNywxIDUuNSwxIEwxLjUsMSBaIE0xLjUsMCBMNS41LDAgQzYuMzI4NDI3MTIsLTEuNjY1MzM0NTRlLTE2IDcsMC42NzE1NzI4NzUgNywxLjUgTDcsNS41IEM3LDYuMzI4NDI3MTIgNi4zMjg0MjcxMiw3IDUuNSw3IEwxLjUsNyBDMC42NzE1NzI4NzUsNyAxLjY2NTMzNDU0ZS0xNiw2LjMyODQyNzEyIDAsNS41IEwwLDEuNSBDLTguMzI2NjcyNjhlLTE3LDAuNjcxNTcyODc1IDAuNjcxNTcyODc1LDEuNjY1MzM0NTRlLTE2IDEuNSwwIFogTTEyLjY5NDQxNTQsMS4xMTE4MDg5IEMxMi41ODcwMjEyLDEuMDA0NDE0NzIgMTIuNDEyOTAwOCwxLjAwNDQxNDcyIDEyLjMwNTUwNjcsMS4xMTE4MDg5IEwxMC4xMTIwNjE0LDMuMzA1MjU0MTMgQzEwLjAwNDY2NzMsMy40MTI2NDgzMSAxMC4wMDQ2NjczLDMuNTg2NzY4NjggMTAuMTEyMDYxNCwzLjY5NDE2Mjg2IEwxMi4zMDU1MDY3LDUuODg3NjA4MSBDMTIuNDEyOTAwOCw1Ljk5NTAwMjI4IDEyLjU4NzAyMTIsNS45OTUwMDIyOCAxMi42OTQ0MTU0LDUuODg3NjA4MSBMMTQuODg3ODYwNiwzLjY5NDE2Mjg2IEMxNC45OTUyNTQ4LDMuNTg2NzY4NjggMTQuOTk1MjU0OCwzLjQxMjY0ODMxIDE0Ljg4Nzg2MDYsMy4zMDUyNTQxMyBMMTIuNjk0NDE1NCwxLjExMTgwODkgWiBNMTMuNDAxNTIyMiwwLjQwNDcwMjExOCBMMTUuNTk0OTY3NCwyLjU5ODE0NzM1IEMxNi4wOTI4ODU5LDMuMDk2MDY1ODIgMTYuMDkyODg1OSwzLjkwMzM1MTE3IDE1LjU5NDk2NzQsNC40MDEyNjk2NCBMMTMuNDAxNTIyMiw2LjU5NDcxNDg4IEMxMi45MDM2MDM3LDcuMDkyNjMzMzUgMTIuMDk2MzE4NCw3LjA5MjYzMzM1IDExLjU5ODM5OTksNi41OTQ3MTQ4OCBMOS40MDQ5NTQ2NSw0LjQwMTI2OTY0IEM4LjkwNzAzNjE4LDMuOTAzMzUxMTcgOC45MDcwMzYxOCwzLjA5NjA2NTgyIDkuNDA0OTU0NjUsMi41OTgxNDczNSBMMTEuNTk4Mzk5OSwwLjQwNDcwMjExOCBDMTIuMDk2MzE4NCwtMC4wOTMyMTYzNTQzIDEyLjkwMzYwMzcsLTAuMDkzMjE2MzU0MyAxMy40MDE1MjIyLDAuNDA0NzAyMTE4IFogTTEuNSwxMCBDMS4yMjM4NTc2MywxMCAxLDEwLjIyMzg1NzYgMSwxMC41IEwxLDE0LjUgQzEsMTQuNzc2MTQyNCAxLjIyMzg1NzYzLDE1IDEuNSwxNSBMNS41LDE1IEM1Ljc3NjE0MjM3LDE1IDYsMTQuNzc2MTQyNCA2LDE0LjUgTDYsMTAuNSBDNiwxMC4yMjM4NTc2IDUuNzc2MTQyMzcsMTAgNS41LDEwIEwxLjUsMTAgWiBNMS41LDkgTDUuNSw5IEM2LjMyODQyNzEyLDkgNyw5LjY3MTU3Mjg4IDcsMTAuNSBMNywxNC41IEM3LDE1LjMyODQyNzEgNi4zMjg0MjcxMiwxNiA1LjUsMTYgTDEuNSwxNiBDMC42NzE1NzI4NzUsMTYgMS42NjUzMzQ1NGUtMTYsMTUuMzI4NDI3MSAwLDE0LjUgTDAsMTAuNSBDLTguMzI2NjcyNjhlLTE3LDkuNjcxNTcyODggMC42NzE1NzI4NzUsOSAxLjUsOSBaIE0xMC41LDEwIEMxMC4yMjM4NTc2LDEwIDEwLDEwLjIyMzg1NzYgMTAsMTAuNSBMMTAsMTQuNSBDMTAsMTQuNzc2MTQyNCAxMC4yMjM4NTc2LDE1IDEwLjUsMTUgTDE0LjUsMTUgQzE0Ljc3NjE0MjQsMTUgMTUsMTQuNzc2MTQyNCAxNSwxNC41IEwxNSwxMC41IEMxNSwxMC4yMjM4NTc2IDE0Ljc3NjE0MjQsMTAgMTQuNSwxMCBMMTAuNSwxMCBaIE0xMC41LDkgTDE0LjUsOSBDMTUuMzI4NDI3MSw5IDE2LDkuNjcxNTcyODggMTYsMTAuNSBMMTYsMTQuNSBDMTYsMTUuMzI4NDI3MSAxNS4zMjg0MjcxLDE2IDE0LjUsMTYgTDEwLjUsMTYgQzkuNjcxNTcyODgsMTYgOSwxNS4zMjg0MjcxIDksMTQuNSBMOSwxMC41IEM5LDkuNjcxNTcyODggOS42NzE1NzI4OCw5IDEwLjUsOSBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4='); --icon-Wrap: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTksMTIgTDksMTMgTDguMzA3ODIzNjYsMTEuOTYxNzM1NSBDOC4xMjcwNjQ4MywxMS44ODY0MTQ4IDgsMTEuNzA4MDQ2MiA4LDExLjUgQzgsMTEuMjkxOTUzOCA4LjEyNzA2NDgzLDExLjExMzU4NTIgOC4zMDc4MjI3NCwxMS4wMzgyNjU5IEw5LDEwIEw5LDExIEwxMS4zMjYwODcsMTEgQzEyLjI0NjY0MTUsMTEgMTMsMTAuMjIwMjczOSAxMyw5LjI1IEMxMyw4LjI3OTcyNjA1IDEyLjI0NjY0MTUsNy41IDExLjMyNjA4Nyw3LjUgTDIuNSw3LjUgQzIuMjIzODU3NjMsNy41IDIsNy4yNzYxNDIzNyAyLDcgQzIsNi43MjM4NTc2MyAyLjIyMzg1NzYzLDYuNSAyLjUsNi41IEwxMS4zMjYwODcsNi41IEMxMi44MDY3NzA1LDYuNSAxNCw3LjczNDk5MjU3IDE0LDkuMjUgQzE0LDEwLjc2NTAwNzQgMTIuODA2NzcwNSwxMiAxMS4zMjYwODcsMTIgTDksMTIgWiBNMi41LDIgTDEzLjUsMiBDMTMuNzc2MTQyNCwyIDE0LDIuMjIzODU3NjMgMTQsMi41IEMxNCwyLjc3NjE0MjM3IDEzLjc3NjE0MjQsMyAxMy41LDMgTDIuNSwzIEMyLjIyMzg1NzYzLDMgMiwyLjc3NjE0MjM3IDIsMi41IEMyLDIuMjIzODU3NjMgMi4yMjM4NTc2MywyIDIuNSwyIFogTTIuNSwxMSBMNS41LDExIEM1Ljc3NjE0MjM3LDExIDYsMTEuMjIzODU3NiA2LDExLjUgQzYsMTEuNzc2MTQyNCA1Ljc3NjE0MjM3LDEyIDUuNSwxMiBMMi41LDEyIEMyLjIyMzg1NzYzLDEyIDIsMTEuNzc2MTQyNCAyLDExLjUgQzIsMTEuMjIzODU3NiAyLjIyMzg1NzYzLDExIDIuNSwxMSBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4='); diff --git a/static/img/get-started.png b/static/img/get-started.png new file mode 100644 index 00000000..1462879a Binary files /dev/null and b/static/img/get-started.png differ diff --git a/static/img/tutorial-screenshot.png b/static/img/tutorial-screenshot.png new file mode 100644 index 00000000..57be7591 Binary files /dev/null and b/static/img/tutorial-screenshot.png differ diff --git a/static/img/youtube-screenshot.png b/static/img/youtube-screenshot.png new file mode 100644 index 00000000..fa678281 Binary files /dev/null and b/static/img/youtube-screenshot.png differ diff --git a/static/ui-icons/Login/LoginStreamline.svg b/static/ui-icons/Login/LoginStreamline.svg index 6a7c849a..15e5a0d9 100644 --- a/static/ui-icons/Login/LoginStreamline.svg +++ b/static/ui-icons/Login/LoginStreamline.svg @@ -1,7 +1,12 @@ - - + + + + - - + + + + + diff --git a/static/ui-icons/Login/LoginUnify.svg b/static/ui-icons/Login/LoginUnify.svg index 48d7d260..ab3f37e6 100644 --- a/static/ui-icons/Login/LoginUnify.svg +++ b/static/ui-icons/Login/LoginUnify.svg @@ -1,5 +1,12 @@ - - - + + + + + + + + + + diff --git a/static/ui-icons/Login/LoginVisualize.svg b/static/ui-icons/Login/LoginVisualize.svg index f9884240..e15dea58 100644 --- a/static/ui-icons/Login/LoginVisualize.svg +++ b/static/ui-icons/Login/LoginVisualize.svg @@ -1,7 +1,14 @@ - - - - - + + + + + + + + + + + + diff --git a/static/ui-icons/UI/Skip.svg b/static/ui-icons/UI/Skip.svg new file mode 100644 index 00000000..c9e55c49 --- /dev/null +++ b/static/ui-icons/UI/Skip.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/ui-icons/UI/Star.svg b/static/ui-icons/UI/Star.svg new file mode 100644 index 00000000..1c8a5400 --- /dev/null +++ b/static/ui-icons/UI/Star.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/ui-icons/UI/VideoPlay.svg b/static/ui-icons/UI/VideoPlay.svg new file mode 100644 index 00000000..f94d390a --- /dev/null +++ b/static/ui-icons/UI/VideoPlay.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/ui-icons/UI/VideoPlay2.svg b/static/ui-icons/UI/VideoPlay2.svg new file mode 100644 index 00000000..11c593e7 --- /dev/null +++ b/static/ui-icons/UI/VideoPlay2.svg @@ -0,0 +1,3 @@ + + + diff --git a/test/fixtures/docs/GristNewUserInfo.grist b/test/fixtures/docs/GristNewUserInfo.grist index 1594cde8..546865db 100644 Binary files a/test/fixtures/docs/GristNewUserInfo.grist and b/test/fixtures/docs/GristNewUserInfo.grist differ diff --git a/test/nbrowser/DocTutorial.ts b/test/nbrowser/DocTutorial.ts index 70d1cc98..1c7d3655 100644 --- a/test/nbrowser/DocTutorial.ts +++ b/test/nbrowser/DocTutorial.ts @@ -13,22 +13,27 @@ describe('DocTutorial', function () { let doc: DocCreationInfo; let api: UserAPI; let ownerSession: gu.Session; + let editorSession: gu.Session; let viewerSession: gu.Session; let oldEnv: EnvironmentSnapshot; - const cleanup = setupTestSuite({team: true}); + const cleanup = setupTestSuite({samples: true, team: true}); before(async () => { + ownerSession = await gu.session().customTeamSite('templates').user('support').login(); + doc = await ownerSession.tempDoc(cleanup, 'DocTutorial.grist', {load: false}); oldEnv = new EnvironmentSnapshot(); process.env.GRIST_UI_FEATURES = 'tutorials'; + process.env.GRIST_TEMPLATE_ORG = 'templates'; + process.env.GRIST_ONBOARDING_TUTORIAL_DOC_ID = doc.id; await server.restart(); - ownerSession = await gu.session().teamSite.user('support').login(); - doc = await ownerSession.tempDoc(cleanup, 'DocTutorial.grist'); + api = ownerSession.createHomeApi(); await api.updateDoc(doc.id, {type: 'tutorial'}); await api.updateDocPermissions(doc.id, {users: { 'anon@getgrist.com': 'viewers', 'everyone@getgrist.com': 'viewers', + [gu.translateUser('user1').email]: 'editors', }}); }); @@ -43,20 +48,25 @@ describe('DocTutorial', function () { }); it('shows a tutorial card', async function() { - await viewerSession.loadRelPath('/'); + await viewerSession.loadDocMenu('/'); + assert.isTrue(await driver.find('.test-onboarding-tutorial-card').isDisplayed()); + assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '0%'); + }); + + it('can dismiss tutorial card', async function() { + await driver.find('.test-onboarding-dismiss-cards').click(); + assert.isFalse(await driver.find('.test-onboarding-tutorial-card').isPresent()); + await driver.navigate().refresh(); await gu.waitForDocMenuToLoad(); - await gu.skipWelcomeQuestions(); + assert.isFalse(await driver.find('.test-onboarding-tutorial-card').isPresent()); + }); - assert.isTrue(await driver.find('.test-tutorial-card-content').isDisplayed()); - // Can dismiss it. - await driver.find('.test-tutorial-card-close').click(); - assert.isFalse((await driver.findAll('.test-tutorial-card-content')).length > 0); - // When dismissed, we can see link in the menu. + it('shows a link to tutorial', async function() { assert.isTrue(await driver.find('.test-dm-basic-tutorial').isDisplayed()); }); it('redirects user to log in', async function() { - await viewerSession.loadDoc(`/doc/${doc.id}`, {wait: false}); + await driver.find('.test-dm-basic-tutorial').click(); await gu.checkLoginPage(); }); }); @@ -65,41 +75,24 @@ describe('DocTutorial', function () { let forkUrl: string; before(async () => { - ownerSession = await gu.session().teamSite.user('user1').login({showTips: true}); + editorSession = await gu.session().customTeamSite('templates').user('user1').login({showTips: true}); + await editorSession.loadDocMenu('/'); + await driver.executeScript('resetDismissedPopups();'); + await gu.waitForServer(); }); afterEach(() => gu.checkForErrors()); it('shows a tutorial card', async function() { - await ownerSession.loadRelPath('/'); - await gu.waitForDocMenuToLoad(); - await gu.skipWelcomeQuestions(); - - // Make sure we have clean start. - await driver.executeScript('resetDismissedPopups();'); - await gu.waitForServer(); - await driver.navigate().refresh(); - await gu.waitForDocMenuToLoad(); - - // Make sure we see the card. - assert.isTrue(await driver.find('.test-tutorial-card-content').isDisplayed()); - - // And can dismiss it. - await driver.find('.test-tutorial-card-close').click(); - assert.isFalse((await driver.findAll('.test-tutorial-card-content')).length > 0); - - // When dismissed, we can see link in the menu. - assert.isTrue(await driver.find('.test-dm-basic-tutorial').isDisplayed()); - - // Prefs are preserved after reload. - await driver.navigate().refresh(); - await gu.waitForDocMenuToLoad(); - assert.isFalse((await driver.findAll('.test-tutorial-card-content')).length > 0); - assert.isTrue(await driver.find('.test-dm-basic-tutorial').isDisplayed()); + assert.isTrue(await driver.find('.test-onboarding-tutorial-card').isDisplayed()); + await gu.waitToPass(async () => + assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '0%'), + 2000 + ); }); it('creates a fork the first time the document is opened', async function() { - await ownerSession.loadDoc(`/doc/${doc.id}`); + await driver.find('.test-dm-basic-tutorial').click(); await driver.wait(async () => { forkUrl = await driver.getCurrentUrl(); return /~/.test(forkUrl); @@ -274,7 +267,7 @@ describe('DocTutorial', function () { }); it('does not show the GristDocTutorial page or table to non-editors', async function() { - viewerSession = await gu.session().teamSite.user('user2').login(); + viewerSession = await gu.session().customTeamSite('templates').user('user2').login(); await viewerSession.loadDoc(`/doc/${doc.id}`); assert.deepEqual(await gu.getPageNames(), ['Page 1', 'Page 2']); await driver.find('.test-tools-raw').click(); @@ -300,7 +293,7 @@ describe('DocTutorial', function () { otherForkUrl = await driver.getCurrentUrl(); return /~/.test(forkUrl); }); - ownerSession = await gu.session().teamSite.user('user1').login(); + editorSession = await gu.session().customTeamSite('templates').user('user1').login(); await driver.navigate().to(otherForkUrl!); assert.match(await driver.findWait('.test-error-header', 2000).getText(), /Access denied/); await driver.navigate().to(forkUrl); @@ -424,7 +417,7 @@ describe('DocTutorial', function () { it('remembers the last slide the user had open', async function() { await driver.find('.test-doc-tutorial-popup-slide-3').click(); - // There's a 1000ms debounce in place for updates to the last slide. + // There's a 1000ms debounce in place when updating tutorial progress. await driver.sleep(1000 + 250); await gu.waitForServer(); await driver.navigate().refresh(); @@ -446,7 +439,7 @@ describe('DocTutorial', function () { await gu.getCell(0, 1).click(); await gu.sendKeys('Redacted', Key.ENTER); await gu.waitForServer(); - await ownerSession.loadDoc(`/doc/${doc.id}`); + await editorSession.loadDoc(`/doc/${doc.id}`); let currentUrl: string; await driver.wait(async () => { currentUrl = await driver.getCurrentUrl(); @@ -456,7 +449,20 @@ describe('DocTutorial', function () { assert.deepEqual(await gu.getVisibleGridCells({cols: [0], rowNums: [1]}), ['Redacted']); }); + it('tracks completion percentage', async function() { + await driver.find('.test-doc-tutorial-popup-end-tutorial').click(); + await gu.waitForServer(); + await gu.waitForDocMenuToLoad(); + await gu.waitToPass(async () => + assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '15%'), + 2000 + ); + await driver.find('.test-dm-basic-tutorial').click(); + await gu.waitForDocToLoad(); + }); + it('skips starting or resuming a tutorial if the open mode is set to default', async function() { + ownerSession = await gu.session().customTeamSite('templates').user('support').login(); await ownerSession.loadDoc(`/doc/${doc.id}/m/default`); assert.deepEqual(await gu.getPageNames(), ['Page 1', 'Page 2', 'GristDocTutorial']); await driver.find('.test-tools-raw').click(); @@ -467,11 +473,14 @@ describe('DocTutorial', function () { }); it('can restart tutorials', async function() { - // Simulate that the tutorial has been updated since it was forked. - await api.updateDoc(doc.id, {name: 'DocTutorial V2'}); - await api.applyUserActions(doc.id, [['AddTable', 'NewTable', [{id: 'A'}]]]); + // Update the tutorial as the owner. + await driver.find('.test-bc-doc').doClick(); + await driver.sendKeys('DocTutorial V2', Key.ENTER); + await gu.waitForServer(); + await gu.addNewTable(); - // Load the fork of the tutorial. + // Switch back to the editor's fork of the tutorial. + editorSession = await gu.session().customTeamSite('templates').user('user1').login(); await driver.navigate().to(forkUrl); await gu.waitForDocToLoad(); await driver.findWait('.test-doc-tutorial-popup', 2000); @@ -503,10 +512,13 @@ describe('DocTutorial', function () { // Check that changes made to the tutorial since it was last started are included. assert.equal(await driver.find('.test-doc-tutorial-popup-header').getText(), 'DocTutorial V2'); - assert.deepEqual(await gu.getPageNames(), ['Page 1', 'Page 2', 'GristDocTutorial', 'NewTable']); + assert.deepEqual(await gu.getPageNames(), ['Page 1', 'Page 2', 'GristDocTutorial', 'Table1']); }); - it('allows editors to replace original', async function() { + it('allows owners to replace original', async function() { + ownerSession = await gu.session().customTeamSite('templates').user('support').login(); + await ownerSession.loadDoc(`/doc/${doc.id}`); + // Make an edit to one of the tutorial slides. await gu.openPage('GristDocTutorial'); await gu.getCell(1, 1).click(); @@ -532,7 +544,7 @@ describe('DocTutorial', function () { await gu.waitForServer(); // Switch to another user and restart the tutorial. - viewerSession = await gu.session().teamSite.user('user2').login(); + viewerSession = await gu.session().customTeamSite('templates').user('user2').login(); await viewerSession.loadDoc(`/doc/${doc.id}`); await driver.findWait('.test-doc-tutorial-popup-restart', 2000).click(); await driver.find('.test-modal-confirm').click(); @@ -554,13 +566,22 @@ describe('DocTutorial', function () { await driver.find('.test-doc-tutorial-popup-next').click(); await gu.waitForDocMenuToLoad(); assert.match(await driver.getCurrentUrl(), /o\/docs\/$/); + await gu.waitToPass(async () => + assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '0%'), + 2000 + ); + await ownerSession.loadDocMenu('/'); + await gu.waitToPass(async () => + assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '100%'), + 2000 + ); }); }); describe('without tutorial flag set', function () { before(async () => { await api.updateDoc(doc.id, {type: null}); - ownerSession = await gu.session().teamSite.user('user1').login(); + ownerSession = await gu.session().customTeamSite('templates').user('support').login(); await ownerSession.loadDoc(`/doc/${doc.id}`); }); @@ -568,7 +589,7 @@ describe('DocTutorial', function () { it('shows the GristDocTutorial page and table', async function() { assert.deepEqual(await gu.getPageNames(), - ['Page 1', 'Page 2', 'GristDocTutorial', 'NewTable']); + ['Page 1', 'Page 2', 'GristDocTutorial', 'Table1']); await gu.openPage('GristDocTutorial'); assert.deepEqual( await gu.getVisibleGridCells({cols: [1, 2], rowNums: [1]}), diff --git a/test/nbrowser/Features.ts b/test/nbrowser/Features.ts index a2682716..120a53f4 100644 --- a/test/nbrowser/Features.ts +++ b/test/nbrowser/Features.ts @@ -33,6 +33,7 @@ describe('Features', function () { it('can be disabled with the GRIST_HIDE_UI_ELEMENTS env variable', async function () { process.env.GRIST_UI_FEATURES = 'helpCenter,tutorials'; process.env.GRIST_HIDE_UI_ELEMENTS = 'templates'; + process.env.GRIST_ONBOARDING_TUTORIAL_DOC_ID = 'tutorialDocId'; await server.restart(); await session.loadDocMenu('/'); assert.isTrue(await driver.find('.test-left-feedback').isDisplayed()); diff --git a/test/nbrowser/HomeIntro.ts b/test/nbrowser/HomeIntro.ts index 92ee9c58..cf14676f 100644 --- a/test/nbrowser/HomeIntro.ts +++ b/test/nbrowser/HomeIntro.ts @@ -52,8 +52,8 @@ describe('HomeIntro', function() { freshAccount: true, }); - // Open doc-menu and dismiss the welcome questions popup - await session.loadDocMenu('/', 'skipWelcomeQuestions'); + // Open doc-menu and skip onboarding + await session.loadDocMenu('/', 'skipOnboarding'); // Reload the doc-menu and dismiss the coaching call popup await session.loadDocMenu('/'); @@ -83,7 +83,7 @@ describe('HomeIntro', function() { await session.resetSite(); // Open doc-menu - await session.loadDocMenu('/', 'skipWelcomeQuestions'); + await session.loadDocMenu('/'); // Check message specific to logged-in user and an empty team site. assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.orgName}`)); diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index 7eea6f44..fe36fbe0 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -826,8 +826,23 @@ export async function loadDoc(relPath: string, wait: boolean = true): Promise { +/** + * Load a DocMenu on a site. + * + * If loading for a potentially first-time user, you may give 'skipOnboarding' for second + * argument to skip the onboarding flow, if it gets shown. + */ +export async function loadDocMenu(relPath: string, wait: boolean|'skipOnboarding' = true): Promise { await driver.get(`${server.getHost()}${relPath}`); + if (wait === 'skipOnboarding') { + const first = await Promise.race([ + driver.findWait('.test-onboarding-page', 2000), + driver.findWait('.test-dm-doclist', 2000), + ]); + if (await first.matches('.test-onboarding-page')) { + await skipOnboarding(); + } + } if (wait) { await waitForDocMenuToLoad(); } } @@ -2105,7 +2120,6 @@ export class Session { freshAccount?: boolean, isFirstLogin?: boolean, showTips?: boolean, - skipTutorial?: boolean, // By default true userName?: string, email?: string, retainExistingLogin?: boolean}) { @@ -2129,11 +2143,6 @@ export class Session { } await server.simulateLogin(this.settings.name, this.settings.email, this.settings.orgDomain, {isFirstLogin: false, cacheCredentials: true, ...options}); - - if (options?.skipTutorial ?? true) { - await dismissTutorialCard(); - } - return this; } @@ -2172,17 +2181,20 @@ export class Session { } // Load a DocMenu on a site. - // If loading for a potentially first-time user, you may give 'skipWelcomeQuestions' for second - // argument to dismiss the popup with welcome questions, if it gets shown. - public async loadDocMenu(relPath: string, wait: boolean|'skipWelcomeQuestions' = true) { + // If loading for a potentially first-time user, you may give 'skipOnboarding' for second + // argument to skip the onboarding flow, if it gets shown. + public async loadDocMenu(relPath: string, wait: boolean|'skipOnboarding' = true) { await this.loadRelPath(relPath); - if (wait) { await waitForDocMenuToLoad(); } - - if (wait === 'skipWelcomeQuestions') { - // When waitForDocMenuToLoad() returns, welcome questions should also render, so that we - // don't need to wait extra for them. - await skipWelcomeQuestions(); + if (wait === 'skipOnboarding') { + const first = await Promise.race([ + driver.findWait('.test-onboarding-page', 2000), + driver.findWait('.test-dm-doclist', 2000), + ]); + if (await first.matches('.test-onboarding-page')) { + await skipOnboarding(); + } } + if (wait) { await waitForDocMenuToLoad(); } } public async loadRelPath(relPath: string) { @@ -3151,21 +3163,6 @@ export async function getFilterMenuState(): Promise { })); } -/** - * Dismisses any tutorial card that might be active. - */ -export async function dismissTutorialCard() { - // If there is something in our way, we can't do it. - if (await driver.find('.test-welcome-questions').isPresent()) { - return; - } - if (await driver.find('.test-tutorial-card-close').isPresent()) { - if (await driver.find('.test-tutorial-card-close').isDisplayed()) { - await driver.find('.test-tutorial-card-close').click(); - } - } -} - /** * Dismisses coaching call if needed. */ @@ -3370,11 +3367,14 @@ export async function setRangeFilterBound(minMax: 'min'|'max', value: string|{re } } -export async function skipWelcomeQuestions() { - if (await driver.find('.test-welcome-questions').isPresent()) { - await driver.sendKeys(Key.ESCAPE); - assert.equal(await driver.find('.test-welcome-questions').isPresent(), false); - } +/** + * Skips the onboarding page that's shown to users on their first visit to the + * doc menu. + */ +export async function skipOnboarding() { + await driver.findWait('.test-onboarding-page', 2000); + await waitForServer(); + await driver.navigate().refresh(); } /**