diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/assets.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/assets.ts new file mode 100644 index 000000000..c3c006b9a --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/assets.ts @@ -0,0 +1,129 @@ +export const journalPaperLight = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +export const journalPaperDark = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/index.tsx similarity index 98% rename from packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.tsx rename to packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/index.tsx index a278efca0..fb00d3b8b 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/index.tsx @@ -28,6 +28,8 @@ import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react'; import * as styles from './journal.css'; +import { JournalTemplateOnboarding } from './template-onboarding'; +import { JournalTemplateSetting } from './template-setting'; /** * @internal @@ -156,12 +158,14 @@ export const EditorJournalPanel = () => { cellSize={34} /> + {journalDate ? ( <> ) : null} + ); }; diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.css.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/journal.css.ts similarity index 98% rename from packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.css.ts rename to packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/journal.css.ts index 5a037341b..0ee24eccc 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.css.ts +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/journal.css.ts @@ -36,9 +36,12 @@ const interactive = style({ }); export const calendar = style({ padding: '16px', + paddingBottom: 0, + marginBottom: 10, selectors: { '&[data-mobile=true]': { padding: '8px 16px', + marginBottom: 0, }, }, }); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-onboarding.css.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-onboarding.css.ts new file mode 100644 index 000000000..6b0470772 --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-onboarding.css.ts @@ -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[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, + }, + }, +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-onboarding.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-onboarding.tsx new file mode 100644 index 000000000..2b0e51a37 --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-onboarding.tsx @@ -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(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 ( +
+
+

+ {t['com.affine.template-journal-onboarding.title']()} +

+ + } + > + + + + } + onClick={onDismiss} + /> +
+
+ ); +}; + +const JournalPaper = memo(function JournalPaper() { + const ref = useRef(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 ( +
+ ); +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-setting.css.ts b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-setting.css.ts new file mode 100644 index 000000000..4e3b634f5 --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-setting.css.ts @@ -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, +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-setting.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-setting.tsx new file mode 100644 index 000000000..aaac9dbc4 --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal/template-setting.tsx @@ -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 ( +
+ + } + > + + +
+ ); +}; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 4328b4324..8d8b78437 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1632,6 +1632,8 @@ "Template": "Template", "com.affine.template-list.empty": "No 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.journal": "Template for journal", "com.affine.settings.workspace.template.journal-desc": "Select a template for your journal",