Summary: A new onboarding page is now shown to all new users visiting the doc menu for the first time. Tutorial cards on the doc menu have been replaced with a new version that tracks completion progress, alongside a new card that opens the orientation video. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4296pull/1118/head
@ -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;
|
||||
`);
|
@ -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<UserPrefs>): 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<string>;
|
||||
role: Observable<string>;
|
||||
useCases: Array<Observable<boolean>>;
|
||||
useOther: Observable<string>;
|
||||
}
|
||||
|
||||
interface VideoState {
|
||||
watched: Observable<boolean>;
|
||||
}
|
||||
|
||||
export class OnboardingPage extends Disposable {
|
||||
private _steps: Array<Step>;
|
||||
private _stepIndex: Observable<number> = 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<number>) {
|
||||
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};
|
||||
}
|
||||
`);
|
@ -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;
|
||||
}
|
||||
}
|
||||
`);
|
@ -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<UserPrefs>): 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<UserPrefs>) {
|
||||
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<boolean>[], otherText: Observable<string>) {
|
||||
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;
|
||||
}
|
||||
`);
|
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 180 KiB |
After Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 615 B |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 359 B |