feat(core): sidebar journal panel template onboarding and setting (#9680)
close AF-2108
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -28,6 +28,8 @@ import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
|||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './journal.css';
|
import * as styles from './journal.css';
|
||||||
|
import { JournalTemplateOnboarding } from './template-onboarding';
|
||||||
|
import { JournalTemplateSetting } from './template-setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@@ -156,12 +158,14 @@ export const EditorJournalPanel = () => {
|
|||||||
cellSize={34}
|
cellSize={34}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<JournalTemplateOnboarding />
|
||||||
{journalDate ? (
|
{journalDate ? (
|
||||||
<>
|
<>
|
||||||
<JournalConflictBlock date={journalDate} />
|
<JournalConflictBlock date={journalDate} />
|
||||||
<JournalDailyCountBlock date={journalDate} />
|
<JournalDailyCountBlock date={journalDate} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
<JournalTemplateSetting />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -36,9 +36,12 @@ const interactive = style({
|
|||||||
});
|
});
|
||||||
export const calendar = style({
|
export const calendar = style({
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
|
paddingBottom: 0,
|
||||||
|
marginBottom: 10,
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-mobile=true]': {
|
'&[data-mobile=true]': {
|
||||||
padding: '8px 16px',
|
padding: '8px 16px',
|
||||||
|
marginBottom: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
type Timeline = {
|
||||||
|
duration: string;
|
||||||
|
delay: string;
|
||||||
|
easing: string;
|
||||||
|
fill?: 'forwards' | 'backwards' | 'both' | 'none';
|
||||||
|
keyframes: Parameters<typeof keyframes>[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeline for the onboarding animation
|
||||||
|
*/
|
||||||
|
export const timeline: Record<'container' | 'card' | 'paper', Timeline> = {
|
||||||
|
container: {
|
||||||
|
duration: '0.23s',
|
||||||
|
delay: '0s',
|
||||||
|
easing: 'ease-out',
|
||||||
|
keyframes: {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: 140 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
duration: '0.5s',
|
||||||
|
delay: '0.1s',
|
||||||
|
easing: 'cubic-bezier(0,1.35,.17,.96)',
|
||||||
|
fill: 'forwards',
|
||||||
|
keyframes: {
|
||||||
|
from: { transform: 'translateY(100%) scale(0.24)' },
|
||||||
|
to: { transform: 'translateY(0) scale(1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
duration: '0.5s',
|
||||||
|
delay: '0.3s',
|
||||||
|
easing: 'cubic-bezier(0,1.06,0,1.09)',
|
||||||
|
fill: 'forwards',
|
||||||
|
keyframes: {
|
||||||
|
from: { transform: 'translate(100px, 100px) rotate(50deg)' },
|
||||||
|
to: { transform: 'translate(22px, 42px) rotate(-8.71deg)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const animation = (tl: Timeline) => ({
|
||||||
|
animationName: keyframes(tl.keyframes),
|
||||||
|
animationDuration: tl.duration,
|
||||||
|
animationTimingFunction: tl.easing,
|
||||||
|
animationDelay: tl.delay,
|
||||||
|
animationFillMode: tl.fill,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingLeft: 16,
|
||||||
|
paddingRight: 16,
|
||||||
|
paddingBottom: 8,
|
||||||
|
marginBottom: 10,
|
||||||
|
width: '100%',
|
||||||
|
height: timeline.container.keyframes.to.height,
|
||||||
|
overflow: 'hidden',
|
||||||
|
...animation(timeline.container),
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[data-animation-played="true"]': {
|
||||||
|
animation: 'none',
|
||||||
|
height: timeline.container.keyframes.to.height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const card = style({
|
||||||
|
padding: '12px 0px 14px 12px',
|
||||||
|
width: '100%',
|
||||||
|
height: 124,
|
||||||
|
borderRadius: 12,
|
||||||
|
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
|
backgroundColor: cssVarV2.layer.background.secondary,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
|
||||||
|
transform: timeline.card.keyframes.from.transform,
|
||||||
|
...animation(timeline.card),
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[data-animation-played="true"]': {
|
||||||
|
animation: 'none',
|
||||||
|
transform: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: '24px',
|
||||||
|
fontWeight: 600,
|
||||||
|
maxWidth: 'calc(100% - 115px)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const close = style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 12,
|
||||||
|
right: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const menu = style({
|
||||||
|
width: 280,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const paper = style({
|
||||||
|
width: 124,
|
||||||
|
height: 124,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
transformOrigin: '0% 100%',
|
||||||
|
transform: timeline.paper.keyframes.from.transform,
|
||||||
|
...animation(timeline.paper),
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[data-animation-played="true"]': {
|
||||||
|
animation: 'none',
|
||||||
|
transform: timeline.paper.keyframes.to.transform,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { Button, IconButton, Menu } from '@affine/component';
|
||||||
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
|
import { GlobalStateService } from '@affine/core/modules/storage';
|
||||||
|
import { TemplateDocService } from '@affine/core/modules/template-doc';
|
||||||
|
import { TemplateListMenuContentScrollable } from '@affine/core/modules/template-doc/view/template-list-menu';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { CloseIcon, TemplateIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { journalPaperDark, journalPaperLight } from './assets';
|
||||||
|
import * as styles from './template-onboarding.css';
|
||||||
|
|
||||||
|
const dismissedKey = 'journal-template-onboarding-dismissed';
|
||||||
|
// to make sure the animation won't re-play until page is reloaded
|
||||||
|
let animationPlayed = false;
|
||||||
|
|
||||||
|
export const JournalTemplateOnboarding = () => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const globalState = useService(GlobalStateService).globalState;
|
||||||
|
const featureFlagService = useService(FeatureFlagService);
|
||||||
|
const templateDocService = useService(TemplateDocService);
|
||||||
|
const enableTemplate = useLiveData(
|
||||||
|
featureFlagService.flags.enable_template_doc.$
|
||||||
|
);
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const dismissed = useLiveData(
|
||||||
|
useMemo(
|
||||||
|
() => LiveData.from(globalState.watch(dismissedKey), false),
|
||||||
|
[globalState]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDismiss = useCallback(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) {
|
||||||
|
globalState.set(dismissedKey, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const animation = container.animate(
|
||||||
|
[
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
height: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
marginBottom: 0,
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ duration: 280, easing: 'cubic-bezier(.35,.58,.01,1)', fill: 'forwards' }
|
||||||
|
);
|
||||||
|
animation.onfinish = () => {
|
||||||
|
globalState.set(dismissedKey, true);
|
||||||
|
};
|
||||||
|
}, [globalState]);
|
||||||
|
|
||||||
|
const updateJournalTemplate = useCallback(
|
||||||
|
(templateId: string) => {
|
||||||
|
templateDocService.setting.updateJournalTemplateDocId(templateId);
|
||||||
|
},
|
||||||
|
[templateDocService.setting]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dismissed || !enableTemplate) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
data-animation-played={animationPlayed}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
<div className={styles.card} data-animation-played={animationPlayed}>
|
||||||
|
<p className={styles.title}>
|
||||||
|
{t['com.affine.template-journal-onboarding.title']()}
|
||||||
|
</p>
|
||||||
|
<Menu
|
||||||
|
contentOptions={{ className: styles.menu, align: 'end' }}
|
||||||
|
items={
|
||||||
|
<TemplateListMenuContentScrollable
|
||||||
|
onSelect={updateJournalTemplate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="primary" prefix={<TemplateIcon />}>
|
||||||
|
{t['com.affine.template-journal-onboarding.select']()}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
<JournalPaper />
|
||||||
|
<IconButton
|
||||||
|
size="16"
|
||||||
|
className={styles.close}
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
onClick={onDismiss}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const JournalPaper = memo(function JournalPaper() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const paper = ref.current;
|
||||||
|
if (!paper) return;
|
||||||
|
const onAnimationEnd = () => (animationPlayed = true);
|
||||||
|
paper.addEventListener('animationend', onAnimationEnd, { once: true });
|
||||||
|
return () => {
|
||||||
|
paper.removeEventListener('animationend', onAnimationEnd);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={styles.paper}
|
||||||
|
data-animation-played={animationPlayed}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: resolvedTheme === 'dark' ? journalPaperDark : journalPaperLight,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px 16px',
|
||||||
|
borderTop: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trigger = style({
|
||||||
|
padding: '2px 4px',
|
||||||
|
borderRadius: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const menu = style({
|
||||||
|
width: 280,
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Button, Menu } from '@affine/component';
|
||||||
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
|
import { TemplateDocService } from '@affine/core/modules/template-doc';
|
||||||
|
import { TemplateListMenuContentScrollable } from '@affine/core/modules/template-doc/view/template-list-menu';
|
||||||
|
import { TemplateIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './template-setting.css';
|
||||||
|
|
||||||
|
export const JournalTemplateSetting = () => {
|
||||||
|
const templateDocService = useService(TemplateDocService);
|
||||||
|
const docDisplayService = useService(DocDisplayMetaService);
|
||||||
|
const journalTemplateDocId = useLiveData(
|
||||||
|
templateDocService.setting.journalTemplateDocId$
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = useLiveData(
|
||||||
|
useMemo(() => {
|
||||||
|
return journalTemplateDocId
|
||||||
|
? docDisplayService.title$(journalTemplateDocId)
|
||||||
|
: null;
|
||||||
|
}, [docDisplayService, journalTemplateDocId])
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateJournalTemplate = useCallback(
|
||||||
|
(templateId: string) => {
|
||||||
|
templateDocService.setting.updateJournalTemplateDocId(templateId);
|
||||||
|
},
|
||||||
|
[templateDocService.setting]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Menu
|
||||||
|
contentOptions={{ className: styles.menu }}
|
||||||
|
items={
|
||||||
|
<TemplateListMenuContentScrollable onSelect={updateJournalTemplate} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
prefix={<TemplateIcon />}
|
||||||
|
className={styles.trigger}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1632,6 +1632,8 @@
|
|||||||
"Template": "Template",
|
"Template": "Template",
|
||||||
"com.affine.template-list.empty": "No template",
|
"com.affine.template-list.empty": "No template",
|
||||||
"com.affine.template-list.create-new": "Create new template",
|
"com.affine.template-list.create-new": "Create new template",
|
||||||
|
"com.affine.template-journal-onboarding.title": "Set a Template for the Journal",
|
||||||
|
"com.affine.template-journal-onboarding.select": "Select",
|
||||||
"com.affine.settings.workspace.template.title": "My Templates",
|
"com.affine.settings.workspace.template.title": "My Templates",
|
||||||
"com.affine.settings.workspace.template.journal": "Template for journal",
|
"com.affine.settings.workspace.template.journal": "Template for journal",
|
||||||
"com.affine.settings.workspace.template.journal-desc": "Select a template for your journal",
|
"com.affine.settings.workspace.template.journal-desc": "Select a template for your journal",
|
||||||
|
|||||||
Reference in New Issue
Block a user