diff --git a/packages/frontend/core/src/components/affine/onboarding/animate-in-tooltip.css.tsx b/packages/frontend/core/src/components/affine/onboarding/animate-in-tooltip.css.tsx new file mode 100644 index 0000000000..7ec4e0306b --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/animate-in-tooltip.css.tsx @@ -0,0 +1,31 @@ +import { keyframes, style } from '@vanilla-extract/css'; + +import { onboardingVars } from './style.css'; + +const fadeIn = keyframes({ + from: { opacity: 0, pointerEvents: 'none' }, + to: { opacity: 1, pointerEvents: 'auto' }, +}); + +export const tooltip = style({ + width: 500, + textAlign: 'center', + fontFamily: 'var(--affine-font-family)', + fontSize: '20px', + lineHeight: '28px', + fontWeight: 600, + textShadow: '0px 0px 4px rgba(66, 65, 73, 0.14)', + color: 'white', + opacity: 0, + animation: `${fadeIn} 1s ease forwards`, + animationDelay: onboardingVars.animateIn.tooltipShowUpDelay, +}); + +export const next = style({ + position: 'absolute', + top: 0, + right: 0, + opacity: 0, + animation: `${fadeIn} 1s ease forwards`, + animationDelay: onboardingVars.animateIn.nextButtonShowUpDelay, +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/animate-in-tooltip.tsx b/packages/frontend/core/src/components/affine/onboarding/animate-in-tooltip.tsx new file mode 100644 index 0000000000..85fe7640f9 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/animate-in-tooltip.tsx @@ -0,0 +1,19 @@ +import { Button } from '@affine/component'; + +import * as styles from './animate-in-tooltip.css'; + +export const AnimateInTooltip = ({ onNext }: { onNext: () => void }) => { + return ( + <> +
+ AFFiNE is a workspace with fully merged docs,
+ whiteboards and databases +
+
+ +
+ + ); +}; diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/article-0.tsx b/packages/frontend/core/src/components/affine/onboarding/articles/article-0.tsx new file mode 100644 index 0000000000..78cf736549 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/articles/article-0.tsx @@ -0,0 +1,276 @@ +import { CounterNote } from '../switch-widgets/counter-note'; +import type { OnboardingBlockOption } from '../types'; +import bookmark1png from './assets/article-0-bookmark-1.png'; +import bookmark2png from './assets/article-0-bookmark-2.png'; +import embed1png from './assets/article-0-embed-1.png'; + +export const article0: Array = [ + { + children:

HOWTO: Be more productive

, + offset: { x: -150, y: 0 }, + }, + { + bg: '#f5f5f5', + children: ( + <> +

+ “With all the time you spend watching TV,” he tells me, “you could + have written a novel by now.” It’s hard to disagree with the sentiment + — writing a novel is undoubtedly a better use of time than watching TV + — but what about the hidden assumption? Such comments imply that time + is “fungible” — that time spent watching TV can just as easily be + spent writing a novel. And sadly, that’s just not the case. +

+

+ Time has various levels of quality. If I’m walking to the subway + station and I’ve forgotten my notebook, then it’s pretty hard for me + to write more than a couple paragraphs. And it’s tough to focus when + you keep getting interrupted. There’s also a mental component: + sometimes I feel happy and motivated and ready to work on something, + but other times I feel so sad and tired I can only watch TV. +

+ + ), + offset: { x: -120, y: 80 }, + sub: { + children: ( + + ), + enterDelay: 300, + position: {}, + style: { + bottom: 'calc(100% + 20px)', + left: -40, + }, + edgelessOnly: true, + }, + }, + { + bg: '#F9E8FF', + children: ( + <> + +

+ If you want to be more productive then, you have to recognize this + fact and deal with it. First, you have to make the best of each kind + of time. And second, you have to try to make your time higher-quality. +

+

Spend time efficiently

+ + ), + offset: { x: 250, y: 100 }, + }, + + { + bg: '#E1EFFF', + children: ( + <> +

Choose good problems

+

+ Life is short (or so I’m told) so why waste it doing something dumb? + It’s easy to start working on something because it’s convenient, but + you should always be questioning yourself about it. Is there something + more important you can work on? Why don’t you do that instead? Such + questions are hard to face up to (eventually, if you follow this rule, + you’ll have to ask yourself why you’re not working on the most + important problem in the world) but each little step makes you more + productive. +

+

+ + This isn’t to say that all your time should be spent on the most + important problem in the world. Mine certainly isn’t (after all, I’m + writing this essay). But it’s definitely the standard against which + I measure my life. + +

+ + ), + offset: { x: -600, y: -130 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 800, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + bg: '#DFF4E8', + children: ( + <> +

Have a bunch of them

+

+ Another common myth is that you’ll get more done if you pick one + problem and focus on it exclusively. I find this is hardly ever true. + Just this moment for example, I’m trying to fix my posture, exercise + some muscles, drink some fluids, clean off my desk, IM with my + brother, and write this essay. Over the course the day, I’ve worked on + this essay, read a book, had some food, answered some email, chatted + with friends, done some shopping, worked on a couple other essays, + backed up my hard drive, and organized my book list. In the past week + I’ve worked on several different software projects, read several + different books, studied a couple different programming languages, + moved some of my stuff, and so on. +

+

+ Having a lot of different projects gives you work for different + qualities of time. Plus, you’ll have other things to work on if you + get stuck or bored (and that can give your mind time to unstick + yourself). +

+

+ It also makes you more creative. Creativity comes from applying things + you learn in other fields to the field you work in. If you have a + bunch of different projects going in different fields, then you have + many more ideas you can apply. +

+ + ), + offset: { x: -50, y: -50 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 1200, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + bg: '#DFF4F3', + children: ( + <> +

Make a list

+

+ Coming up with a bunch of different things to work on shouldn’t be + hard — most people have tons of stuff they want to get done. But if + you try to keep it all in your head it quickly gets overwhelming. The + psychic pressure of having to remember all of it can make you crazy. + The solution is again simple: write it down. +

+

+ Once you have a list of all the things you want to do, you can + organize it by kind. For example, my list is programming, writing, + thinking, errands, reading, listening, and watching (in that order). +

+

+ Most major projects involve a bunch of these different tasks. Writing + this, for example, involves reading about other procrastination + systems, thinking up new sections of the article, cleaning up + sentences, emailing people with questions, and so on, all in addition + to the actual work of writing the text. Each task can go under the + appropriate section, so that you can do it when you have the right + kind of time. +

+ + ), + offset: { x: 800, y: -400 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 1500, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + // children: , + children: , + edgelessOnly: true, + position: { x: 700, y: 230 }, + fromPosition: { x: 1000, y: 0 }, + }, + + { + bg: '#FFE1E1', + children: ( + <> +

Integrate the list with your life

+

+ Once you have this list, the problem becomes remembering to look at + it. And the best way to remember to look at it is to make looking at + it what you would do anyway. For example, I keep a stack of books on + my desk, with the ones I’m currently reading on top. When I need a + book to read, I just grab the top one off the stack. +

+

+ I do the same thing with TV/movies. Whenever I hear about a movie I + should watch, I put it in a special folder on my computer. Now + whenever I feel like watching TV, I just open up that folder. +

+

+ I’ve also thought about some more intrusive ways of doing this. For + example, a web page that pops up with a list of articles in my “to + read” folder whenever I try to check some weblogs. Or maybe even a + window that pops up with work suggestions occasionally for me to see + when I’m goofing off. +

+

+ Making the best use of the time you have can only get you so far. The + much more important problem is making more higher quality time for + yourself. Most people’s time is eaten up by things like school and + work. Obviously if you attend one of these, you should stop. But what + else can you do? +

+ + ), + offset: { x: 1200, y: -1600 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 1500, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + // children: , + children: , + edgelessOnly: true, + position: { x: 1050, y: 630 }, + fromPosition: { x: 1400, y: 630 }, + enterDelay: 200, + }, +]; diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/article-1.tsx b/packages/frontend/core/src/components/affine/onboarding/articles/article-1.tsx new file mode 100644 index 0000000000..009cfd649f --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/articles/article-1.tsx @@ -0,0 +1,294 @@ +import clsx from 'clsx'; + +import { CounterNote } from '../switch-widgets/counter-note'; +import { PageLink } from '../switch-widgets/page-link'; +import type { OnboardingBlockOption } from '../types'; +import bookmark1png from './assets/article-1-bookmark-1.png'; +import illustration1png from './assets/article-1-illustration-1.png'; +import Article1Illustration2 from './assets/article-1-illustration-2'; +import { hr, link, quote } from './blocks.css'; + +export const article1: Array = [ + { + children:

This is Local-first software

, + offset: { x: -600, y: 0 }, + }, + { + bg: '#F5F5F5', + children: ( + <> +

Local-first software

+

You own your data, in spite of the cloud

+

+ Cloud apps like Google Docs and{' '} + Trello are popular because they enable + real-time collaboration with colleagues, and they make it easy for us + to access our work from all of our devices. However, by centralizing + data storage on servers, cloud apps also take away ownership and + agency from users.{' '} + + If a service shuts down, the software stops functioning, and data + created with that software is lost. + +

+ + ), + offset: { x: -570, y: 80 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 300, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + bg: '#F3F0FF', + children: ( + <> + +

+ If you are an entrepreneur interested in building developer + infrastructure, all of the above suggests an interesting market + opportunity: “Firebase for CRDTs.” +

+

+ In this article we propose local-first software{' '} + of principles for software that enables both collaboration +

+ + ), + offset: { x: -570, y: 200 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 600, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + bg: '#DFF4F3', + children: ( + <> +

+ We survey existing approaches to data storage and sharing, ranging + from email attachments to web apps to Firebase-backed mobile apps, and + we examine the trade-offs of each. We look at Conflict-free Replicated + Data Types (CRDTs): data structures that are multi-user from the + ground up while also being fundamentally local and private. CRDTs have + the potential to be a foundational technology for realizing + local-first software. +

+
+

+ We share some of our findings from developing local-first software + prototypes at Ink & Switch over the course of + several years. These experiments test the viability of CRDTs in + practice, and explore the user interface challenges for this new data + model. Lastly, we suggest some next steps for moving towards + local-first software: for researchers, for app developers, and a + startup opportunity for entrepreneurs. +

+ + ), + offset: { x: 290, y: -140 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 900, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + bg: '#FFF4D8', + children: ( + <> +

+ This article has also been published in PDF{' '} + format in the proceedings of the{' '} + Onward! 2019 conference. Please cite it as: +

+

+ Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark + McGranaghan. Local-first software: you own your data, in spite of the + cloud. 2019 ACM SIGPLAN International Symposium on New Ideas, New + Paradigms, and Reflections on Programming and Software (Onward!), + October 2019, pages 154–178.{' '} + doi:10.1145/3359591.3359737 +

+ +

+ We welcome your feedback: @inkandswitch or + hello@inkandswitch.com. +

+ + ), + offset: { x: 350, y: -850 }, + }, + + { + bg: '#E1EFFF', + children: ( + <> +

Contents

+

+ Motivation: collaboration and ownership. +
+ Seven ideals for local-first software +

+ +
    +
  1. No spinners: your work at your fingertips
  2. +
  3. + Your work is not trapped on one device +
  4. +
  5. + The network is optional +
  6. +
  7. Seamless collaboration with your colleagues
  8. +
  9. + The Long Now +
  10. +
  11. Security and privacy by default
  12. +
  13. You retain ultimate ownership and control
  14. +
+ +

Existing data storage and sharing models

+
    +
  • How application architecture affects user experience
  • +
  • Developer infrastructure for building apps
  • +
+ +

Towards a better future

+
    +
  • CRDTs as a foundational technology
  • +
  • Ink & Switch prototypes
  • +
  • How you can help
  • +
+ +

Conclusions

+
    +
  • Acknowledgments
  • +
+ + ), + offset: { x: 300, y: -250 }, + customStyle: { edgeless: { width: 500 } }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 1200, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + bg: '#FFE1E1', + children: ( + <> +

Motivation: collaboration and ownership

+

+ It’s amazing how easily we can collaborate online nowadays. We use + Google Docs to collaborate on documents, spreadsheets and + presentations; in Figma we work together on user interface designs; we + communicate with colleagues using Slack; we track tasks in Trello; and + so on. We depend on these and many other online services, e.g. for + taking notes, planning projects or events, remembering contacts, and a + whole raft of business uses. +

+

+ Today’s cloud apps offer big benefits compared to earlier generations + of software: seamless collaboration, and being able to access data + from any device. As we run more and more of our lives and work through + these cloud apps, they become more and more critical to us. The more + time we invest in using one of these apps, the more valuable the data + in it becomes to us. +

+

+ However, in our research we have spoken to a lot of creative + professionals, and in that process we have also learned about the + downsides of cloud apps. +

+

+ When you have put a lot of creative energy and effort into making + something, you tend to have a deep emotional attachment to it. If you + do creative work, this probably seems familiar. (When we say “creative + work,” we mean not just visual art, or music, or poetry — many other + activities, such as explaining a technical topic, implementing an + intricate algorithm, designing a user interface, or figuring out how + to lead a team towards some goal are also creative efforts.) +

+ + ), + offset: { x: 900, y: -950 }, + sub: { + children: ( + + ), + edgelessOnly: true, + enterDelay: 1500, + position: {}, + style: { bottom: 'calc(100% + 20px)', left: -40 }, + }, + }, + + { + children: , + edgelessOnly: true, + position: { x: -600, y: 1000 }, + fromPosition: { x: -1000, y: 1500 }, + enterDelay: 250, + }, + + { + children: , + edgelessOnly: true, + position: { x: 1200, y: 500 }, + fromPosition: { x: 1800, y: -100 }, + enterDelay: 200, + }, +]; diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/article-2.tsx b/packages/frontend/core/src/components/affine/onboarding/articles/article-2.tsx new file mode 100644 index 0000000000..8e5780fbc7 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/articles/article-2.tsx @@ -0,0 +1,174 @@ +import type { OnboardingBlockOption } from '../types'; +import embed1png from './assets/article-2-embed-1.png'; +import illustration1png from './assets/article-2-illustration-1.jpg'; +import illustration2png from './assets/article-2-illustration-2.jpg'; +import note1png from './assets/article-2-note-1.png'; +import note2png from './assets/article-2-note-2.png'; + +export const article2: Array = [ + { + children: ( +

+ Learning with earning with +
retrieval practice +

+ ), + offset: { x: -824, y: 0 }, + }, + { + bg: '#DFF4E8', + children: ( +

+ Are there any specific techniques to make the process of learning more + effective? +

+ ), + offset: { x: -800, y: 100 }, + }, + + { + bg: '#DFF4E8', + children: ( + <> +

+ Students often re-read, underline, or highlight materials, thinking + that it will help them learn better. But, the best method for really + turning information into long-term memory is to use what is called + ‘retrieval practice’. +

+ +

+ It simply means trying to retrieve the information from your own + brain, instead of looking at a sheet of paper. When you have really + learnt something, you’ve created sets of links between the neurons in + long-term memory. Each time you tug on those sets of links and bring + them back to mind from your memory, you strengthen them. This is why, + using flashcards, teaching others, or trying to retrieve key ideas + from your own mind, after having glanced at a page or your notes, can + be valuable. +

+ + ), + offset: { x: -800, y: 100 }, + }, + + { + bg: '#FFF4D8', + children:

How can a student learn more effectively?

, + offset: { x: 100, y: -300 }, + }, + + { + bg: '#FFF4D8', + children: ( + <> +

+ The best method for more efficient learning is to avoid multitasking. + The Pomodoro Technique helps with this. To do a Pomodoro, just put + away all distractions (no pop-upson your computer or dings from your + phone!), set a timer for 25 minutes, and focus as fully as you can for + those 25 minutes, bringing your thoughts back to the task if you find + them wandering. At the end of the 25 minutes, give yourself a + five-minutes break and do not indulge in anything where you have to + focus on (no checking e-mails, for example – but making a cup of tea + is just fine). Repeat the Pomodoro/break three or four times, and + then, take a 30-minutes break – or wrap your studies up for the day! +

+ + + ), + offset: { x: 100, y: -300 }, + }, + + { + bg: '#DFF4F3', + children: ( + <> +

How can students remember more and forget less?

+

+ We all wish we had much better memories, but, if you remember too + perfectly, it makes it harder to revisit and update your learning – to + be more flexible on the face of new information, or to change and + adapt if you’ve got something wrong. If you want to remember more, + retrieval practice with spaced repetition, that is, spacing your + retrieval practice out over a number of days – is your best bet. + Researchers sug-gest that you should retrieve the information when you + are about to forget it, which is a tricky bit of timing to figure out. +

+

How can we make online learning more effective for students?

+

+ It can be hard to write a textbook for a class; so, most professors + use professional textbooks to supplement their teachings. Similarly, + it can be hard to create high-quality video materials. I believe + online teaching will continue to become more common post-COVID – the + genie is out of the bottle as, now, students realise that online + learning can be more convenient than face-to-face classes. But, + students still like their teachers – real professors, who they can + interact with. So, real face-to-face instructors will always have a + place. +

+ + ), + offset: { x: -750, y: -530 }, + }, + + { + bg: '#F3F0FF', + children: ( + <> +

+ Do you think students should use social media, like YouTube, for + learning? +

+

+ There are some excellent learning materials available on YouTube, but, + students may have to spend a lot of time wading through the dreck to + find what they are looking for. Students – and all of us – need to be + a little wary of spending too much time on social media. It can be the + equivalent of parking yourself in front of the television and + indulging in hours of mindless entertainment – except that social + media can also be an echo chamber that can push us to unwittingly + become more extreme in our beliefs. +

+ + ), + offset: { x: 150, y: -680 }, + }, + + { + children: , + edgelessOnly: true, + position: { x: -300, y: 0 }, + fromPosition: { x: 300, y: -300 }, + }, + + { + children: , + edgelessOnly: true, + position: { x: -360, y: -20 }, + fromPosition: { x: -360, y: -100 }, + enterDelay: 300, + customStyle: { + page: { + transform: 'rotate(-10deg) translateY(-100px)', + }, + }, + }, + + { + children: , + edgelessOnly: true, + position: { x: 0, y: 0 }, + fromPosition: { x: 2000, y: -2000 }, + }, +]; diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/article-3.tsx b/packages/frontend/core/src/components/affine/onboarding/articles/article-3.tsx new file mode 100644 index 0000000000..5158a83d41 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/articles/article-3.tsx @@ -0,0 +1,198 @@ +import type { OnboardingBlockOption } from '../types'; +import bookmark1png from './assets/article-3-bookmark-1.png'; +import illustration1jpg from './assets/article-3-illustration-1.jpg'; +import illustration2jpg from './assets/article-3-illustration-2.jpg'; +import illustration3jpg from './assets/article-3-illustration-3.jpg'; +import illustration4jpg from './assets/article-3-illustration-4.jpg'; +import illustration5jpg from './assets/article-3-illustration-5.jpg'; + +export const article3: Array = [ + { + children: ( + + ), + edgelessOnly: true, + position: { x: -780, y: 216 }, + fromPosition: { x: -1500, y: 216 }, + enterDelay: 200, + }, + + { + children: , + edgelessOnly: true, + position: { x: 500, y: 200 }, + fromPosition: { x: 1000, y: -200 }, + enterDelay: 200, + leaveDelay: 100, + }, + + { + children:

Breath of the Wild: Redefining Game Design

, + offset: { x: -400, y: 0 }, + customStyle: { + edgeless: { whiteSpace: 'nowrap' }, + }, + }, + { + bg: '#E1EFFF', + children: ( + <> +

Introduction

+

+ At GDC 2017, Hidemaro Fujibayashi, Satoru Takizawa, and Takuhiro Dohta + from Nintendo shared their insights on The Legend of Zelda: Breath of + the Wild's groundbreaking game mechanics. One standout feature is + the "multiplicative gameplay," which empowers players to + interact with the game world in diverse ways, leading to surprising + outcomes. +

+ +

Mechanics

+

+ Multiplicative gameplay works like a magical concoction, where + players' actions, terrain, and items combine to create a range of + unexpected results. For instance, players can set trees ablaze to + create a fiery barrier against enemies, detonate bombs to carve new + paths, or soar through the skies with a glider for a bird's-eye + view of the world. +

+

+ To achieve multiplicative gameplay, the development team made + significant changes to the game's terrain system, physics system, + and action system. +

+ + ), + offset: { x: -400, y: 0 }, + }, + + { + bg: '#F5F5F5', + children: ( + <> +

Terrain

+

+ The terrain system enables players to climb any surface, whether + it's a wall, a tree, or a rock, granting them the freedom to + explore every nook and cranny of the game world. +

+ + + ), + offset: { x: 480, y: -250 }, + }, + + { + bg: '#FFEACA', + children: ( + <> +

Physics

+

+ Unlike traditional games, the physics system in Breath of the Wild is + more open, allowing players to leverage natural phenomena to solve + puzzles or create new gameplay opportunities. For instance, players + can ignite trees to create barriers of fire or use bombs to open up + new pathways. +

+ + + ), + offset: { x: -500, y: -400 }, + }, + + { + bg: '#DFF4E8', + children: ( + <> +

Action

+

+ The action system offers more flexibility than in traditional games, + empowering players to choose how they want to interact with the game + world. Whether it's wielding a sword, a shield, or a bow and + arrow, players have the freedom to approach challenges in their own + unique way. +

+ + + ), + offset: { x: 400, y: -700 }, + }, + + { + bg: '#FFE1E1', + children: ( + <> +

Validation

+

+ Takizawa also outlined the process for validating multiplicative + gameplay. He believes that it can be achieved through the following + steps: +

+ +

+ List all possible behaviors, terrain features, and items. For example, + in Breath of the Wild, players can engage in activities such as + climbing, gliding, swimming, and combat; terrain includes mountains, + forests, plains, and lakes; and items encompass weapons, tools, and + food. +

+

+ Analyze the interactions between these elements. For example, players + can use fire to ignite trees, creating a barrier of fire to impede + enemies' progress. +

+

+ Test the results of these interactions. For example, the development + team can assess whether players can safely navigate through a wall of + fire. +

+

+ By following these steps, developers can verify whether multiplicative + gameplay will yield the desired outcomes. +

+ + ), + offset: { x: -440, y: -870 }, + }, + + { + bg: '#F3F0FF', + children: ( + <> +

Conclusion

+

+ Multiplicative gameplay is a potent tool for crafting more captivating + and immersive gaming experiences. It empowers players to explore the + game world in creative and flexible ways, leading to fresh and + unexpected discoveries. +

+ + ), + offset: { x: 450, y: -1400 }, + }, +]; diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/article-4.tsx b/packages/frontend/core/src/components/affine/onboarding/articles/article-4.tsx new file mode 100644 index 0000000000..657137a0e5 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/articles/article-4.tsx @@ -0,0 +1,247 @@ +import { ShadowSticker } from '../switch-widgets/shadow-sticker'; +import type { OnboardingBlockOption } from '../types'; +import bookmark1png from './assets/article-4-bookmark-1.png'; +import bookmark2png from './assets/article-4-bookmark-2.png'; +import illustration1jpg from './assets/article-4-illustration-1.jpg'; +import illustration2jpg from './assets/article-4-illustration-2.jpg'; + +export const article4: Array = [ + { + children:

More Is Different

, + offset: { x: -430, y: 0 }, + }, + { + bg: '#FFEACA', + offset: { x: -400, y: 0 }, + children: ( + <> +

+ Broken symmetry and the nature of the hierarchical structure of + science +

+ +

+ The reductionist hypothesis may still be a topic for controversy among + philosophers, but among the great majority of active scientists I + think it is accepted without questions. The workings of our minds and + bodies, and of all the animate or inanimate matter of which we have + any detailed knowledge, are assumed to be controlled by the same set + of fundamental laws, which except under certain extreme conditions we + feel we know pretty well. +

+

+ It seems inevitable to go on uncritically to what appears at first + sight to be an obvious corollary of reductionism: that if everything + obeys the same fundamental laws, then the only scientists who are + studying anything really fundamental are those who are working on + those laws. In practice, that amounts to some astrophysicists, some + elementary particle physicists, some logicians and other + mathematicians, and few others. This point of view, which it is the + main purpose of this article to oppose, is expressed in a rather + well-known passage by Weisskopf (1): +

+ + ), + sub: { + children: ( + + Reductionist hypothesis accepted by most scientists, fundamental laws + in focus. + + ), + edgelessOnly: true, + position: {}, + style: { + right: -20, + bottom: '100%', + transformOrigin: '100% 100%', + }, + customStyle: { + page: { transform: 'scale(0)' }, + edgeless: {}, + }, + enterDelay: 200, + leaveDelay: 100, + }, + }, + + { + bg: '#DFF4F3', + offset: { x: 500, y: -490 }, + children: ( +

+ Looking at the development of science in the Twentieth Century one can + distinguish two trends, which I will call “intensive” and “extensive” + research, lacking a better terminology. In short: intensive research + goes for the fundamental laws, extensive research goes for the + explanation of phenomena in terms of known fundamental laws, As always, + distinctions of this kind are not unambiguous, but they are clear in + most cases. Solid state physics plasma physics, and perhaps also biology + are extensive. High energy physics and a good part of nuclear physics + are intensive research going on than extensive. Once new fundamental + laws are discovered, a large and ever increasing activity begins in + order to apply the discoveries to hitherto unexplained phenomena. Thus, + there are two dimensions to basic research. The frontier of science + extends all along a long line from the newest and most modern intensive + research, over the extensive research recently spawned by the intensive + research of yesterday, to the broad and well developed web of extensive + research activities based on intensive research of past decades +

+ ), + sub: { + children: ( + + Twentieth Century science: intensive vs. extensive research, + fundamental laws' impact. + + ), + position: {}, + style: { + left: 'calc(100% - 50px)', + bottom: '0px', + transformOrigin: '0% 50%', + }, + customStyle: { + page: { transform: 'scale(0)' }, + edgeless: {}, + }, + enterDelay: 300, + leaveDelay: 100, + }, + }, + + { + bg: '#E1EFFF', + offset: { x: -800, y: -280 }, + children: ( + <> +

+ The effectiveness of this message may be indicated by the face that I + heard it quoted recently by a leader in the field of materials + science, who urged the participants at a meeting dedicated to + “fundamental problems in condensed matter physics” to accept that + there were few or no such problems and that nothing was left but + extensive science, which he seemed to equate with device engineering. +

+

+ The main fallacy in this kind of thinking is that the reductionist + hypothesis does not by any means imply a “constructions” one: The + ability to reduce everything to simple fundamental laws does not imply + the ability to start from those laws and reconstruct the universe. In + fact, the more the elementary particle physicists tell us about the + nature of the fundamental laws, the less relevance they seem to have + to the very real problems of the rest of science much less to those of + society. +

+ + ), + sub: { + children: ( + + Misunderstanding: Reductionism doesn't mean reconstructing + complex phenomena from fundamentals. + + ), + position: {}, + style: { + top: 'calc(100% - 30px)', + left: 'calc(100% - 250px)', + transformOrigin: '0 0', + }, + customStyle: { + page: { transform: 'scale(0)' }, + edgeless: {}, + }, + enterDelay: 400, + leaveDelay: 100, + }, + }, + + { + bg: '#FFE1E1', + offset: { x: 580, y: -680 }, + children: ( + <> +

+ The constructionist hypothesis breaks down when confronted with the + twin difficulties of scale and complexity. The behavior of large and + complex aggregates of elementary particles, it turns out, is not to be + understood in terms of a simple extrapolation entirely new properties + appear, and the understanding of the new behaviors requires research + which I think is as fundamental in its nature as any other. That is, + it seems to me that one may array the sciences roughly linearly in a + hierarchy, according to the idea: The elementary entities of science X + obey the laws of science Y. +

+

+ But this hierarchy does not imply that science X is “just applied Y.” + At each stage entirely new laws, concepts and generalizations are + necessary, requiring inspiration and creativity to just as great a + degree as in the previous one. Psychology is not applied biology, nor + is biology applied chemistry. +

+ + ), + + sub: { + children: ( + + Complex systems introduce new properties, demanding fundamental + research beyond reductionism. + + ), + edgelessOnly: true, + position: {}, + style: { + bottom: '100%', + left: '-100px', + transformOrigin: '0% 100%', + }, + customStyle: { + page: { transform: 'scale(0)' }, + edgeless: {}, + }, + enterDelay: 500, + leaveDelay: 100, + }, + }, + + // + { + children: , + edgelessOnly: true, + position: { x: 0, y: 760 }, + }, + + { + children: ( + + ), + edgelessOnly: true, + position: { x: -820, y: 150 }, + fromPosition: { x: -1800, y: 150 }, + enterDelay: 200, + leaveDelay: 200, + sub: { + children: ( + + ), + edgelessOnly: true, + position: {}, + style: { + top: 'calc(100% - 40px)', + left: 'calc(100% - 250px)', + }, + }, + }, +]; diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-bookmark-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-bookmark-1.png new file mode 100644 index 0000000000..f851b00fe8 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-bookmark-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-bookmark-2.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-bookmark-2.png new file mode 100644 index 0000000000..ab343f364c Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-bookmark-2.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-embed-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-embed-1.png new file mode 100644 index 0000000000..587d8fe737 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-0-embed-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-bookmark-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-bookmark-1.png new file mode 100644 index 0000000000..97d9f131fc Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-bookmark-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-illustration-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-illustration-1.png new file mode 100644 index 0000000000..ad608c5803 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-illustration-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-illustration-2.tsx b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-illustration-2.tsx new file mode 100644 index 0000000000..68b68e73c2 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-1-illustration-2.tsx @@ -0,0 +1,34 @@ +import { memo } from 'react'; + +export default memo(function Atricle1Illustration2() { + return ( + + + + + + + + + + ); +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-embed-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-embed-1.png new file mode 100644 index 0000000000..10a2384deb Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-embed-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-illustration-1.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-illustration-1.jpg new file mode 100644 index 0000000000..9a5f86ba06 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-illustration-1.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-illustration-2.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-illustration-2.jpg new file mode 100644 index 0000000000..cb7e33bea4 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-illustration-2.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-note-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-note-1.png new file mode 100644 index 0000000000..1d51ab4769 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-note-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-note-2.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-note-2.png new file mode 100644 index 0000000000..6de96a4785 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-2-note-2.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-bookmark-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-bookmark-1.png new file mode 100644 index 0000000000..35eb2e01b1 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-bookmark-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-1.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-1.jpg new file mode 100644 index 0000000000..1d4d3bc1b2 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-1.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-2.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-2.jpg new file mode 100644 index 0000000000..8f473f59d0 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-2.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-3.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-3.jpg new file mode 100644 index 0000000000..be37b7d5ed Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-3.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-4.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-4.jpg new file mode 100644 index 0000000000..7436423282 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-4.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-5.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-5.jpg new file mode 100644 index 0000000000..9c26eac2b3 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-3-illustration-5.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-bookmark-1.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-bookmark-1.png new file mode 100644 index 0000000000..6c022ccfb2 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-bookmark-1.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-bookmark-2.png b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-bookmark-2.png new file mode 100644 index 0000000000..c6af08f99a Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-bookmark-2.png differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-illustration-1.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-illustration-1.jpg new file mode 100644 index 0000000000..eaa923bd2d Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-illustration-1.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-illustration-2.jpg b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-illustration-2.jpg new file mode 100644 index 0000000000..1e9e153d17 Binary files /dev/null and b/packages/frontend/core/src/components/affine/onboarding/articles/assets/article-4-illustration-2.jpg differ diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/blocks.css.ts b/packages/frontend/core/src/components/affine/onboarding/articles/blocks.css.ts new file mode 100644 index 0000000000..88ef506adf --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/articles/blocks.css.ts @@ -0,0 +1,137 @@ +import { globalStyle, style } from '@vanilla-extract/css'; + +export const block = style({}); +globalStyle(`${block} h1`, { + fontSize: '40px', + fontWeight: '600', + lineHeight: '48px', +}); + +globalStyle(`${block} h2`, { + fontSize: '20px', + fontWeight: '600', + lineHeight: '28px', +}); + +globalStyle(`${block} h3`, { + fontSize: '18px', + fontWeight: '600', + lineHeight: '26px', +}); + +globalStyle(`${block} p`, { + fontSize: '15px', + fontWeight: 400, + lineHeight: '23px', +}); + +globalStyle(`${block} b`, { + // TODO: 500's effect not matching the design, use 600 for now + fontWeight: '600', +}); + +globalStyle(`${block} ol`, { + counterReset: 'section', + listStyleType: 'none', + display: 'flex', + flexDirection: 'column', + gap: '2px', +}); +globalStyle(`${block} ol li, ${block} ul li`, { + fontSize: '15px', + fontWeight: 400, + lineHeight: '23px', +}); +globalStyle(`${block} ol li::before`, { + display: 'inline-block', + counterIncrement: 'section', + content: 'counter(section) "."', + color: '#1C81D9', + width: '24px', + marginRight: '4px', +}); +globalStyle(`${block} ul`, { + listStyleType: 'none', + display: 'flex', + flexDirection: 'column', + gap: '2px', +}); +globalStyle(`${block} ul li`, { + position: 'relative', + paddingLeft: '24px', +}); +globalStyle(`${block} ul li::before`, { + vars: { + '--dot-svg': `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Ccircle cx='2' cy='2' r='2' fill='%231C81D9'/%3E%3C/svg%3E")`, + }, + content: '""', + position: 'absolute', + left: 0, + top: 0, + width: '24px', + height: '24px', + display: 'inline-block', + backgroundImage: 'var(--dot-svg)', + backgroundRepeat: 'no-repeat', + backgroundPosition: '4px 50%', +}); +globalStyle(`${block} img.illustration`, { + borderRadius: '5px', + overflow: 'hidden', +}); + +export const link = style({ + color: '#1E67AF', +}); +export const pageLink = style({ + color: '#424149', + fontWeight: '500', + display: 'inline-flex', + alignItems: 'baseline', + gap: '5px', +}); +export const pageLinkIcon = style({ + color: '#77757D', + alignSelf: 'center', +}); +export const pageLinkLabel = style({ + position: 'relative', + selectors: { + '&::after': { + content: '""', + position: 'absolute', + bottom: '-1px', + left: 0, + height: '1px', + transform: 'scaleY(0.5)', + width: '100%', + backgroundColor: '#E3E2E4', + }, + }, +}); + +export const quote = style({ + paddingLeft: '17px', + position: 'relative', + selectors: { + '&::before': { + position: 'absolute', + content: '""', + top: 0, + left: 0, + height: '100%', + width: '2px', + borderRadius: '1px', + backgroundColor: '#D9D9D9', + }, + }, +}); + +export const hr = style({ + height: '1px', + backgroundColor: '#E3E2E4', + border: 'none', + margin: '18px 0', + width: 'calc(100% - 20px)', + alignSelf: 'center', +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/articles/index.tsx b/packages/frontend/core/src/components/affine/onboarding/articles/index.tsx index 15038bff6b..f1e9ae5e43 100644 --- a/packages/frontend/core/src/components/affine/onboarding/articles/index.tsx +++ b/packages/frontend/core/src/components/affine/onboarding/articles/index.tsx @@ -1,5 +1,11 @@ import { article, articleWrapper, text, title } from '../curve-paper/paper.css'; -import type { ArticleId, ArticleOption } from '../types'; +import type { ArticleId, ArticleOption, EdgelessSwitchState } from '../types'; +// TODO: lazy load +import { article0 } from './article-0'; +import { article1 } from './article-1'; +import { article2 } from './article-2'; +import { article3 } from './article-3'; +import { article4 } from './article-4'; const ids = ['0', '1', '2', '3', '4'] as Array; @@ -127,19 +133,29 @@ const paperBriefs = { '0': (
-

Breath of the Wild: Redefining Game Design

+

HOWTO: Be more productive

“With all the time you spend watching TV,” he tells me, “you could have written a novel by now.” It’s hard to disagree with the sentiment — writing a novel is undoubtedly a better use of time than watching TV - — but what about the hidden assumption? Such comments imply that time - is “fungible” — that time spent watching TV can just as easily be - spent writing a novel. And sadly, that’s just not the case. + — but what about the hidden ...

), - '1': ( + '3': ( +
+
+

Breath of the Wild: Redefining Game Design

+

+ At GDC 2017, Hidemaro Fujibayashi, Satoru Takizawa, and Takuhiro Dohta + from Nintendo shared their insights on The Legend of Zelda: Breath of + the Wild's groundbreaking game mechanics. One standout ... +

+
+
+ ), + '2': (

Learning with earning with retrieval practice

@@ -150,13 +166,12 @@ const paperBriefs = {

Students often re-read, underline, or highlight materials, thinking that it will help them learn better. But, the best method for really - turning information into long-term memory is to use what is called - ‘retrieval practice’. + ...

), - '2': ( + '1': (

@@ -168,14 +183,12 @@ const paperBriefs = { Cloud apps like Google Docs and Trello are popular because they enable real-time collaboration with colleagues, and they make it easy for us to access our work from all of our devices. However, by centralizing - data storage on servers, cloud apps also take away ownership and - agency from users. If a service shuts down, the software stops - functioning, and data created with that software is lost. + ...

), - '3': ( + '4': (

More Is Different

@@ -186,32 +199,49 @@ const paperBriefs = {

The reductionist hypothesis may still be a topic for controversy among philosophers, but among the great majority of active scientists I - think it is accepted without questions. The workings of our minds and - bodies, and of all the animate or inanimate matter of which we have - any detailed knowledge, are assumed to be controlled by the same set - of fundamental laws, which except under certain extreme conditions we - feel we know pretty well. -

-
-
- ), - '4': ( -
-
-

HOWTO: Be more productive

-

- “With all the time you spend watching TV,” he tells me, “you could - have written a novel by now.” It’s hard to disagree with the sentiment - — writing a novel is undoubtedly a better use of time than watching TV - — but what about the hidden assumption? Such comments imply that time - is “fungible” — that time spent watching TV can just as easily be - spent writing a novel. And sadly, that’s just not the case. + think it is accepted without ...

), }; +const contents = { + '0': article0, + '1': article1, + '2': article2, + '3': article3, + '4': article4, +}; + +const states: Partial> = { + '0': { + scale: 0.5, + offsetX: -330, + offsetY: -380, + }, + '1': { + scale: 0.4, + offsetX: -330, + offsetY: -500, + }, + '2': { + scale: 0.45, + offsetX: 0, + offsetY: -380, + }, + '3': { + scale: 0.4, + offsetX: 100, + offsetY: -320, + }, + '4': { + scale: 0.48, + offsetX: 10, + offsetY: -220, + }, +}; + export const articles: Record = ids.reduce( (acc, id) => { return { @@ -221,6 +251,8 @@ export const articles: Record = ids.reduce( location: paperLocations[id], enterOptions: paperEnterAnimations[id], brief: paperBriefs[id], + blocks: contents[id], + initState: states[id], } satisfies ArticleOption, }; }, diff --git a/packages/frontend/core/src/components/affine/onboarding/onboarding.tsx b/packages/frontend/core/src/components/affine/onboarding/onboarding.tsx index dfa83e3f02..79a079bdb7 100644 --- a/packages/frontend/core/src/components/affine/onboarding/onboarding.tsx +++ b/packages/frontend/core/src/components/affine/onboarding/onboarding.tsx @@ -1,25 +1,26 @@ import { type CSSProperties, useCallback, useState } from 'react'; +import { AnimateInTooltip } from './animate-in-tooltip'; import { articles } from './articles'; import { PaperSteps } from './paper-steps'; import * as styles from './style.css'; -import type { ArticleId, ArticleOption } from './types'; +import type { ArticleId, ArticleOption, OnboardingStatus } from './types'; interface OnboardingProps { onOpenApp?: () => void; } -export const Onboarding = (_: OnboardingProps) => { - const [status, setStatus] = useState<{ - activeId: ArticleId | null; - unfoldingId: ArticleId | null; - }>({ activeId: null, unfoldingId: null }); +export const Onboarding = ({ onOpenApp }: OnboardingProps) => { + const [status, setStatus] = useState({ + activeId: null, + unfoldingId: null, + }); const onFoldChange = useCallback((id: ArticleId, v: boolean) => { setStatus(s => { return { activeId: v ? null : s.activeId, - unfoldingId: v ? null : id, + unfoldingId: v ? s.unfoldingId : id, }; }); }, []); @@ -28,7 +29,7 @@ export const Onboarding = (_: OnboardingProps) => { setStatus(s => { return { activeId: v ? null : id, - unfoldingId: s.unfoldingId, + unfoldingId: v ? null : s.unfoldingId, }; }); }, []); @@ -62,15 +63,23 @@ export const Onboarding = (_: OnboardingProps) => { return (
); } )} + +
+ setStatus({ activeId: null, unfoldingId: '4' })} + /> +
); diff --git a/packages/frontend/core/src/components/affine/onboarding/paper-steps.tsx b/packages/frontend/core/src/components/affine/onboarding/paper-steps.tsx index 5432f14bbf..68287b5b24 100644 --- a/packages/frontend/core/src/components/affine/onboarding/paper-steps.tsx +++ b/packages/frontend/core/src/components/affine/onboarding/paper-steps.tsx @@ -1,24 +1,30 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { AnimateIn } from './steps/animate-in'; +import { EdgelessSwitch } from './steps/edgeless-switch'; import { Unfolding } from './steps/unfolding'; -import type { ArticleId, OnboardingStep } from './types'; +import type { ArticleId, OnboardingStatus, OnboardingStep } from './types'; import { type ArticleOption } from './types'; interface PaperStepsProps { show?: boolean; article: ArticleOption; + status: OnboardingStatus; onFoldChange?: (id: ArticleId, v: boolean) => void; onFoldChanged?: (id: ArticleId, v: boolean) => void; + onOpenApp?: () => void; } export const PaperSteps = ({ show, article, + status, onFoldChange, onFoldChanged, + onOpenApp, }: PaperStepsProps) => { const [stage, setStage] = useState('enter'); + const [fold, setFold] = useState(true); const onEntered = useCallback(() => { setStage('unfold'); @@ -26,6 +32,7 @@ export const PaperSteps = ({ const _onFoldChange = useCallback( (v: boolean) => { + setFold(v); onFoldChange?.(article.id, v); }, [onFoldChange, article.id] @@ -34,18 +41,40 @@ export const PaperSteps = ({ const _onFoldChanged = useCallback( (v: boolean) => { onFoldChanged?.(article.id, v); + if (!v) setStage('edgeless-switch'); }, [onFoldChanged, article.id] ); + const onEdgelessSwitchBack = useCallback(() => { + setFold(false); + setStage('unfold'); + // to apply fold animation + setTimeout(() => _onFoldChange(true)); + }, [_onFoldChange]); + + useEffect(() => { + if (stage === 'unfold' && status.unfoldingId === article.id) { + console.log('unfold', article.id); + setFold(false); + } + }, [article.id, stage, status.unfoldingId]); + if (!show) return null; return stage === 'enter' ? ( ) : stage === 'unfold' ? ( - ) : null; + ) : ( + + ); }; diff --git a/packages/frontend/core/src/components/affine/onboarding/steps/animate-in.tsx b/packages/frontend/core/src/components/affine/onboarding/steps/animate-in.tsx index d323da3e59..ec4068a35f 100644 --- a/packages/frontend/core/src/components/affine/onboarding/steps/animate-in.tsx +++ b/packages/frontend/core/src/components/affine/onboarding/steps/animate-in.tsx @@ -13,6 +13,8 @@ interface AnimateInProps { } const easing = 'spring(5, 100, 10, 0)'; +const segments = 4; + const animeSync = (params: Parameters[0]) => { return new Promise(resolve => { anime({ ...params, complete: () => resolve(null) }); @@ -26,9 +28,8 @@ export const AnimateIn = ({ }: AnimateInProps) => { const { id: _id, enterOptions, brief } = article; const id = `onboardingMoveIn${_id}`; - const segments = 4; - const rotateX = enterOptions.curve / segments; + const rotateX = (1.2 * enterOptions.curve) / segments; useEffect(() => { Promise.all([ @@ -39,7 +40,7 @@ export const AnimateIn = ({ delay: enterOptions.delay, }), animeSync({ - targets: `[data-id="${id}"] ${paperStyles.segment}[data-direction="down"]`, + targets: `[data-id="${id}"] .${paperStyles.segment}[data-direction="down"]`, rotateX: [rotateX, 0], easing, delay: enterOptions.delay, diff --git a/packages/frontend/core/src/components/affine/onboarding/steps/edgeless-switch.css.ts b/packages/frontend/core/src/components/affine/onboarding/steps/edgeless-switch.css.ts new file mode 100644 index 0000000000..98dee41b8c --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/steps/edgeless-switch.css.ts @@ -0,0 +1,118 @@ +import { globalStyle, style } from '@vanilla-extract/css'; + +import { onboardingVars } from '../style.css'; + +export const edgelessSwitchWindow = style({ + vars: { '--bg-size': '10px' }, + borderRadius: onboardingVars.paper.r, + backgroundColor: onboardingVars.paper.bg, + position: 'relative', + transition: `width ${onboardingVars.window.transition.size}, height ${onboardingVars.window.transition.size}`, + overflow: 'hidden', + boxShadow: 'var(--affine-shadow-2)', + + fontFamily: 'var(--affine-font-family)', + color: onboardingVars.paper.textColor, + + selectors: { + '&[data-mode="edgeless"]': { + width: onboardingVars.edgeless.w, + height: onboardingVars.edgeless.h, + }, + '&[data-mode="page"]': { + width: onboardingVars.article.w, + height: onboardingVars.article.h, + }, + // grid background + '&::before': { + content: '""', + position: 'absolute', + inset: 0, + + backgroundImage: onboardingVars.canvas.bgImage, + backgroundRepeat: 'repeat', + backgroundSize: 'calc(24px * var(--scale)) calc(24px * var(--scale))', + backgroundPositionX: 'calc(var(--offset-x) * var(--scale))', + backgroundPositionY: 'calc(var(--offset-y) * var(--scale))', + + opacity: 0, + pointerEvents: 'none', + transition: 'opacity 0.3s ease', + }, + '&[data-mode="edgeless"]::before': { + opacity: 1, + }, + }, +}); + +export const toolbar = style({ + position: 'absolute', + bottom: '20px', + left: '50%', + transform: 'translateX(-50%) translateY(200px)', + transition: 'transform 0.3s ease', + + selectors: { + [`${edgelessSwitchWindow}[data-mode="edgeless"] &`]: { + transform: 'translateX(-50%) translateY(0px)', + }, + }, +}); + +export const canvas = style({ + position: 'relative', + width: '100%', + height: '100%', + transform: 'scale(var(--scale)) translate(var(--offset-x), var(--offset-y))', + transition: 'transform 0.36s ease', + display: 'flex', + justifyContent: 'center', + alignItems: 'flex-start', + overflowY: 'visible', + + selectors: { + '[data-scroll="true"] &': { + overflowY: 'auto', + }, + '[data-mode="edgeless"] &': { + cursor: 'grab', + }, + '.grabbing[data-mode="edgeless"] &': { + cursor: 'grabbing', + transition: 'none', + }, + '.scaling[data-mode="edgeless"] &': { + transition: 'none', + }, + }, +}); + +export const page = style({ + width: '800px', + minHeight: onboardingVars.article.h, + paddingTop: '150px', + paddingBottom: '150px', +}); + +export const noDragWrapper = style({ + position: 'absolute', + inset: 0, + pointerEvents: 'none', +}); +globalStyle(`${noDragWrapper} > *`, { + pointerEvents: 'auto', +}); + +export const header = style({ + position: 'absolute', + top: '0', + width: '100%', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', + padding: '24px', + pointerEvents: 'none', +}); +globalStyle(`${header} > *`, { + pointerEvents: 'auto', +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/steps/edgeless-switch.tsx b/packages/frontend/core/src/components/affine/onboarding/steps/edgeless-switch.tsx new file mode 100644 index 0000000000..72d6531990 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/steps/edgeless-switch.tsx @@ -0,0 +1,238 @@ +import { Button } from '@affine/component'; +import { debounce } from 'lodash-es'; +import { + type CSSProperties, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; + +import { OnboardingBlock } from '../switch-widgets/block'; +import { EdgelessSwitchButtons } from '../switch-widgets/switch'; +import { ToolbarSVG } from '../switch-widgets/toolbar'; +import type { + ArticleOption, + EdgelessSwitchMode, + EdgelessSwitchState, +} from '../types'; +import * as styles from './edgeless-switch.css'; + +interface EdgelessSwitchProps { + article: ArticleOption; + onBack?: () => void; + onNext?: () => void; +} + +const offsetXRanges = [-2000, 2000]; +const offsetYRanges = [-2000, 2000]; +const scaleRange = [0.2, 2]; + +const defaultState: EdgelessSwitchState = { + scale: 0.5, + offsetX: 0, + offsetY: 0, +}; + +export const EdgelessSwitch = ({ + article, + onBack, + onNext, +}: EdgelessSwitchProps) => { + const windowRef = useRef(null); + const canvasRef = useRef(null); + const mouseDownRef = useRef(false); + const prevStateRef = useRef( + article.initState ?? null + ); + const enableScrollTimerRef = useRef>(); + const turnOffScalingRef = useRef<() => void>(() => {}); + + const [scrollable, setScrollable] = useState(false); + const [mode, setMode] = useState('page'); + const [state, setState] = useState({ + scale: 1, + offsetX: 0, + offsetY: 0, + }); + + const onSwitchToPageMode = useCallback(() => setMode('page'), []); + const onSwitchToEdgelessMode = useCallback(() => setMode('edgeless'), []); + const toggleGrabbing = useCallback((v: boolean) => { + if (!windowRef.current) return; + windowRef.current.classList.toggle('grabbing', v); + }, []); + const turnOnScaling = useCallback(() => { + if (!windowRef.current) return; + windowRef.current.classList.add('scaling'); + }, []); + + const enableScrollWithDelay = useCallback(() => { + return new Promise(resolve => { + enableScrollTimerRef.current = setTimeout(() => { + setScrollable(true); + resolve(true); + }, 500); + }); + }, []); + const disableScroll = useCallback(() => { + if (enableScrollTimerRef.current) + clearTimeout(enableScrollTimerRef.current); + setScrollable(false); + }, []); + const setStateAndSave = useCallback((state: EdgelessSwitchState) => { + setState(state); + prevStateRef.current = state; + }, []); + const onNextClick = useCallback(() => { + if (mode === 'page') setMode('edgeless'); + else onNext?.(); + }, [mode, onNext]); + + useEffect(() => { + turnOffScalingRef.current = debounce(() => { + if (!windowRef.current) return; + windowRef.current.classList.remove('scaling'); + }, 100); + }, []); + + useEffect(() => { + if (mode === 'page') return; + const canvas = canvasRef.current; + const win = windowRef.current; + if (!win || !canvas) return; + + const onWheel = (e: WheelEvent) => { + turnOnScaling(); + const { deltaY } = e; + const newScale = state.scale - deltaY * 0.001; + const safeScale = Math.max( + Math.min(newScale, scaleRange[1]), + scaleRange[0] + ); + setStateAndSave({ ...state, scale: safeScale }); + turnOffScalingRef.current?.(); + }; + + // TODO: mobile support + const onMouseDown = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (target.closest('[data-no-drag]')) return; + e.preventDefault(); + mouseDownRef.current = true; + toggleGrabbing(true); + }; + const onMouseMove = (e: MouseEvent) => { + if (!mouseDownRef.current) return; + const offsetX = state.offsetX + e.movementX / state.scale; + const offsetY = state.offsetY + e.movementY / state.scale; + + const safeOffsetX = Math.max( + Math.min(offsetX, offsetXRanges[1]), + offsetXRanges[0] + ); + const safeOffsetY = Math.max( + Math.min(offsetY, offsetYRanges[1]), + offsetYRanges[0] + ); + + setStateAndSave({ + scale: state.scale, + offsetX: safeOffsetX, + offsetY: safeOffsetY, + }); + }; + const onMouseUp = (_: MouseEvent) => { + mouseDownRef.current = false; + toggleGrabbing(false); + }; + + win.addEventListener('wheel', onWheel); + win.addEventListener('mousedown', onMouseDown); + window.addEventListener('mouseup', onMouseUp); + window.addEventListener('mousemove', onMouseMove); + + return () => { + win.removeEventListener('wheel', onWheel); + win.removeEventListener('mousedown', onMouseDown); + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + }, [ + mode, + state, + state.offsetX, + state.offsetY, + state.scale, + setStateAndSave, + toggleGrabbing, + turnOnScaling, + ]); + + // to avoid `overflow: auto` clip the content before animation ends + useEffect(() => { + if (mode === 'page') { + enableScrollWithDelay() + .then(() => { + // handle scroll + canvasRef.current?.scrollTo({ top: 0 }); + }) + .catch(console.error); + + setState({ scale: 1, offsetX: 0, offsetY: 0 }); + } else { + disableScroll(); + canvasRef.current?.scrollTo({ top: 0 }); + + // save state when switching between modes + setState(prevStateRef.current ?? defaultState); + } + }, [disableScroll, enableScrollWithDelay, mode]); + + const canvasStyle = { + '--scale': state.scale, + '--offset-x': state.offsetX + 'px', + '--offset-y': state.offsetY + 'px', + } as CSSProperties; + + return ( +
+
+
+ { + /* render blocks */ + article.blocks.map((block, key) => { + return ; + }) + } +
+
+ +
+
+ + + +
+ +
+ +
+
+
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.css.ts b/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.css.ts index 3c4aa5e05b..aefeb25f08 100644 --- a/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.css.ts +++ b/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.css.ts @@ -56,8 +56,8 @@ export const unfoldingWrapper = style([ }, width: onboardingVars.article.w, height: onboardingVars.article.h, - left: `calc(0 - ${onboardingVars.article.w} / 2)`, - top: `calc(0 - ${onboardingVars.article.h} / 2)`, + left: `calc(-${onboardingVars.article.w} / 2)`, + top: `calc(-${onboardingVars.article.h} / 2)`, }, }, }, diff --git a/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.tsx b/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.tsx index f4465a6d14..2a1fa65fa4 100644 --- a/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.tsx +++ b/packages/frontend/core/src/components/affine/onboarding/steps/unfolding.tsx @@ -1,47 +1,53 @@ import clsx from 'clsx'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import type { ArticleOption } from '../types'; import * as styles from './unfolding.css'; interface UnfoldingProps { + fold: boolean; + article: ArticleOption; + initialFold?: boolean; onChange?: (e: boolean) => void; onChanged?: (e: boolean) => void; - article: ArticleOption; } -export const Unfolding = ({ article, onChange, onChanged }: UnfoldingProps) => { - const [fold, setFold] = useState(true); +export const Unfolding = ({ + fold, + article, + onChange, + onChanged, +}: UnfoldingProps) => { + const [folding, setFolding] = useState(fold); const ref = useRef(null); const toggleFold = useCallback(() => { - setFold(!fold); - return !fold; - }, [fold]); + onChange?.(!fold); + }, [fold, onChange]); - const onPaperClick = useCallback(() => { - const isFold = toggleFold(); - onChange?.(isFold); + useEffect(() => { + setFolding(fold); + const paper = ref.current; - if (ref.current) { + if (paper) { const handler = () => { - onChanged?.(isFold); + onChanged?.(fold); }; ref.current.addEventListener('transitionend', handler, { once: true }); - return () => ref.current?.removeEventListener('transitionend', handler); + return () => paper?.removeEventListener('transitionend', handler); } - return null; - }, [toggleFold, onChange, onChanged]); + return () => null; + }, [fold, onChanged]); return (
-
+
{article.brief}
diff --git a/packages/frontend/core/src/components/affine/onboarding/style.css.ts b/packages/frontend/core/src/components/affine/onboarding/style.css.ts index 3c4c010f2d..f6ae03ae60 100644 --- a/packages/frontend/core/src/components/affine/onboarding/style.css.ts +++ b/packages/frontend/core/src/components/affine/onboarding/style.css.ts @@ -3,7 +3,7 @@ import { globalStyle, style } from '@vanilla-extract/css'; // in case that we need to support dark mode later export const onboardingVars = { window: { - bg: 'var(--affine-pure-white)', + bg: 'white', shadow: 'var(--affine-shadow-1)', transition: { size: '0.3s ease', @@ -13,7 +13,7 @@ export const onboardingVars = { w: '230px', h: '302px', r: '8px', - bg: 'var(--affine-pure-white)', + bg: 'white', // textColor: 'var(--affine-light-text-primary-color)', textColor: '#121212', borderColor: '#E3E2E4', @@ -23,7 +23,7 @@ export const onboardingVars = { transformTransition: '0.3s ease', }, web: { - bg: '#fafafa', // TODO: use var + bg: '#fafafa', }, article: { @@ -34,6 +34,27 @@ export const onboardingVars = { w: '1200px', h: '800px', }, + + canvas: { + width: 3500, + height: 3500, + pageBlockWidth: 800, + bgImage: 'radial-gradient(#e6e6e6 1px, #fff 1px)', + }, + + toolbar: { + bg: 'white', + borderColor: '#E3E2E4', + }, + + block: { + transition: '0.5s ease', + }, + + animateIn: { + tooltipShowUpDelay: '5s', + nextButtonShowUpDelay: '20s', + }, }; export const perspective = style({ @@ -86,3 +107,20 @@ export const paperLocation = style({ left: `calc(var(--offset-x) - ${onboardingVars.paper.w} / 2)`, top: `calc(var(--offset-y) - ${onboardingVars.paper.h} / 2)`, }); + +export const tipsWrapper = style({ + position: 'absolute', + width: `calc(${onboardingVars.article.w} - 48px)`, + top: `calc(-${onboardingVars.article.h} / 2 + 24px)`, + pointerEvents: 'none', + display: 'flex', + justifyContent: 'center', + opacity: 0, + transition: '0.3s ease 1s', + selectors: { + '&[data-visible="true"]': { + pointerEvents: 'auto', + opacity: 1, + }, + }, +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/block.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/block.tsx new file mode 100644 index 0000000000..93c1aa234d --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/block.tsx @@ -0,0 +1,68 @@ +import type { CSSProperties } from 'react'; + +import { type EdgelessSwitchMode, type OnboardingBlockOption } from '../types'; +import { onboardingBlock } from './style.css'; + +interface OnboardingBlockProps extends OnboardingBlockOption { + mode: EdgelessSwitchMode; +} + +export const OnboardingBlock = ({ + bg, + mode, + style, + children, + offset, + position, + fromPosition, + enterDelay, + leaveDelay, + edgelessOnly, + customStyle, + sub, +}: OnboardingBlockProps) => { + const baseStyles = { + '--bg': bg, + '--enter-delay': enterDelay ? `${enterDelay}ms` : '0ms', + '--leave-delay': leaveDelay ? `${leaveDelay}ms` : '0ms', + zIndex: position ? 1 : 0, + position: position || fromPosition ? 'absolute' : 'relative', + } as CSSProperties; + + if (mode === 'page') { + if (fromPosition) { + baseStyles.left = fromPosition.x ?? 'unset'; + baseStyles.top = fromPosition.y ?? 'unset'; + } + } else { + if (offset) { + baseStyles.transform = `translate(${offset.x}px, ${offset.y}px)`; + } + if (position) { + baseStyles.left = position.x ?? 'unset'; + baseStyles.top = position.y ?? 'unset'; + } + } + + const blockStyles = { + ...baseStyles, + ...style, + ...customStyle?.[mode], + } as CSSProperties; + + return ( +
{ + e.stopPropagation(); + }} + className={onboardingBlock} + data-mode={mode} + data-bg-mode={bg && mode === 'edgeless'} + data-invisible={mode === 'page' && edgelessOnly} + > + {children} + {sub ? : null} +
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/counter-note.css.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/counter-note.css.tsx new file mode 100644 index 0000000000..dba270c004 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/counter-note.css.tsx @@ -0,0 +1,49 @@ +import { keyframes, style } from '@vanilla-extract/css'; + +export const counterNote = style({ + display: 'flex', + alignItems: 'center', + gap: '8px', +}); +export const count = style({ + width: '50px', + height: '50px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, +}); +export const label = style({ + fontSize: '16px', + fontFamily: '"Orelega One"', + fontWeight: 700, + lineHeight: '18px', + width: 0, + flexGrow: 1, +}); + +const drawCircleLine = keyframes({ + from: { strokeDashoffset: 120 }, + to: { strokeDashoffset: 0 }, +}); +export const circleAnim = style({ + strokeDashoffset: 120, + selectors: { + '[data-mode="edgeless"] &': { + animation: `${drawCircleLine} 0.5s ease forwards`, + }, + }, +}); + +const numberIn = keyframes({ + from: { opacity: 0 }, + to: { opacity: 1 }, +}); +export const fadeInAnim = style({ + opacity: 0, + selectors: { + '[data-mode="edgeless"] &': { + animation: `${numberIn} 0.2s ease forwards`, + }, + }, +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/counter-note.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/counter-note.tsx new file mode 100644 index 0000000000..c498e6f9ec --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/counter-note.tsx @@ -0,0 +1,64 @@ +import clsx from 'clsx'; + +import * as styles from './counter-note.css'; + +interface CounterNoteProps { + index: 1 | 2 | 3 | 4 | 5; + label: string; + width: number; + color?: string; + animationDelay?: number; +} + +const D_MAP = { + 1: 'M20.6505 10.9611C20.6505 11.9045 20.5158 12.9677 20.2462 14.1507C19.9767 15.3187 19.7221 16.3594 19.4825 17.2728C19.2429 18.1862 19.0258 19.0697 18.8311 19.9232C18.6515 20.7768 18.4942 21.4731 18.3595 22.0121C18.2397 22.5362 18.0974 23.0753 17.9327 23.6294C17.6033 24.6925 17.2439 25.2241 16.8545 25.2241C16.5101 25.2241 16.1133 25.007 15.6641 24.5727C15.2298 24.1235 15.0127 23.7192 15.0127 23.3598C15.0127 23.0004 15.1774 22.1769 15.5069 20.8891C15.8363 19.6013 16.1657 17.9167 16.4952 15.8353C16.8396 13.7389 17.0118 11.5601 17.0118 9.29901C17.0118 8.86475 17.2289 8.64763 17.6632 8.64763C18.1124 8.64763 18.7188 8.96208 19.4825 9.591C20.2612 10.2199 20.6505 10.6766 20.6505 10.9611Z', + 2: 'M14.7984 22.0043C17.4788 21.5102 19.5078 21.2631 20.8854 21.2631C22.2781 21.2631 22.9744 21.5626 22.9744 22.1616C22.9744 22.7306 21.7839 23.3445 19.403 24.0034C17.0371 24.6623 15.2177 24.9917 13.9449 24.9917C13.4208 24.9917 12.7844 24.6548 12.0357 23.9809C11.287 23.3071 10.9126 22.7081 10.9126 22.184C10.9126 21.8396 11.871 20.8813 13.7877 19.309C14.5813 18.6501 15.3749 17.9613 16.1686 17.2425C16.9622 16.5088 17.636 15.7601 18.1901 14.9964C18.7591 14.2327 19.0436 13.5514 19.0436 12.9524C19.0436 12.3385 18.7591 12.0315 18.1901 12.0315C17.7558 12.0315 17.3665 12.1962 17.0221 12.5256C16.6927 12.8401 16.4306 13.162 16.2359 13.4915C16.0563 13.8059 15.9215 13.9632 15.8316 13.9632C15.4423 13.9632 14.9482 13.7535 14.3492 13.3342C13.7652 12.9 13.4732 12.5256 13.4732 12.2112C13.4732 11.3576 13.9224 10.549 14.8209 9.78535C15.7343 9.00669 16.8274 8.61735 18.1002 8.61735C19.388 8.61735 20.3988 9.01417 21.1325 9.80781C21.8663 10.5865 22.2331 11.5823 22.2331 12.7952C22.2331 14.4274 21.5293 16.067 20.1218 17.7142C18.7292 19.3614 16.9547 20.7914 14.7984 22.0043Z', + 3: 'M14.1695 17.1104V17.0206C14.1695 16.8409 14.2519 16.6912 14.4166 16.5714C14.6112 16.3318 14.9931 16.0023 15.5621 15.5831C16.1311 15.1638 16.6627 14.7745 17.1569 14.4151C17.666 14.0407 18.1227 13.599 18.527 13.0899C18.9313 12.5807 19.1335 12.0866 19.1335 11.6074C19.1335 11.4277 19.0661 11.3379 18.9313 11.3379C18.4372 11.3379 17.7334 11.5849 16.8199 12.0791C15.9065 12.5583 15.3225 12.7979 15.068 12.7979C14.8134 12.7979 14.4091 12.5358 13.855 12.0117C13.301 11.4726 13.024 11.0758 13.024 10.8213C13.024 10.402 13.3759 9.99768 14.0797 9.60835C14.7834 9.20404 15.5846 8.89707 16.483 8.68743C17.3964 8.46282 18.1976 8.35051 18.8864 8.35051C19.8747 8.35051 20.7282 8.70989 21.447 9.42866C22.1807 10.1474 22.5476 10.9036 22.5476 11.6973C22.5476 12.4909 22.2107 13.3145 21.5368 14.168C20.878 15.0066 20.0544 15.7628 19.0661 16.4366C21.1625 16.9008 22.2107 17.8517 22.2107 19.2892C22.2107 20.2026 21.6791 21.1011 20.6159 21.9846C19.5677 22.8531 18.2949 23.5419 16.7975 24.051C15.3001 24.5601 13.885 24.8147 12.5523 24.8147C10.9351 24.8147 10.1265 24.4628 10.1265 23.759C10.1265 23.3697 11.4741 22.7782 14.1695 21.9846C15.1728 21.7 16.0937 21.3107 16.9322 20.8166C17.7858 20.3224 18.2125 19.8133 18.2125 19.2892C18.2125 19.0047 18.0628 18.7876 17.7633 18.6378C17.4638 18.4731 17.1269 18.3683 16.7526 18.3234C15.6145 18.1886 14.8733 17.9864 14.5289 17.7169C14.2893 17.5672 14.1695 17.365 14.1695 17.1104Z', + 4: 'M20.1218 12.0789C20.4961 12.0789 20.9978 12.3559 21.6267 12.9099C22.2706 13.464 22.5925 13.9207 22.5925 14.2801C22.5925 15.1486 22.0609 17.2375 20.9978 20.5468C19.9346 23.8411 19.1335 25.4883 18.5944 25.4883C18.3099 25.4883 17.9879 25.2786 17.6285 24.8594C17.2841 24.4251 17.1119 24.0582 17.1119 23.7588C17.1119 23.4593 17.1494 23.1448 17.2242 22.8154C17.2991 22.471 17.374 22.1565 17.4489 21.872C17.5387 21.5725 17.666 21.1532 17.8307 20.6142C18.0104 20.0751 18.1526 19.6408 18.2575 19.3114C17.1793 19.6858 16.1162 19.8729 15.068 19.8729C14.0347 19.8729 13.2261 19.5884 12.6421 19.0194C12.0731 18.4504 11.7886 17.6867 11.7886 16.7284C11.7886 16.0395 12.1555 14.9913 12.8892 13.5838C13.6379 12.1762 14.4615 10.9109 15.36 9.78779C16.2584 8.66472 16.8948 8.10319 17.2692 8.10319C17.6585 8.10319 18.0553 8.29037 18.4596 8.66473C18.8789 9.03908 19.0885 9.39098 19.0885 9.72041C19.0885 10.0349 18.3847 11.1879 16.9772 13.1795C15.5846 15.171 14.8883 16.3465 14.8883 16.7059C14.8883 16.8556 14.9182 16.953 14.9781 16.9979C15.038 17.0428 15.1728 17.0653 15.3824 17.0653C15.8915 17.0653 16.6253 16.8631 17.5836 16.4588C18.542 16.0545 19.0586 15.8449 19.1335 15.8299C19.3281 14.7667 19.4255 13.741 19.4255 12.7527C19.4255 12.3035 19.6576 12.0789 20.1218 12.0789Z', + 5: 'M19.5602 8.48552C22.3005 8.48552 23.6707 8.81496 23.6707 9.47382C23.6707 9.75833 23.4386 10.0054 22.9744 10.215C22.4203 10.4846 21.7465 10.7092 20.9528 10.8889C20.1742 11.0686 19.5453 11.2108 19.0661 11.3157C18.6019 11.4205 18.235 11.5103 17.9655 11.5852C17.6959 11.6451 17.4264 11.9521 17.1569 12.5061C16.9023 13.0602 16.775 13.4944 16.775 13.8089C16.775 14.1233 16.9173 14.4528 17.2018 14.7972C17.5013 15.1416 17.8607 15.4785 18.2799 15.8079C18.6992 16.1374 19.1185 16.4818 19.5378 16.8412C20.541 17.6947 21.0427 18.5632 21.0427 19.4467C21.0427 20.8093 20.3688 22.0597 19.0212 23.1977C17.6884 24.3208 16.3782 24.8823 15.0904 24.8823C13.8026 24.8823 12.702 24.5379 11.7886 23.8491C10.8901 23.1453 10.4409 22.3517 10.4409 21.4682C10.4409 21.1837 10.4933 20.9366 10.5981 20.727C10.703 20.5173 10.8452 20.4125 11.0249 20.4125C11.2046 20.4125 11.4367 20.5473 11.7212 20.8168C12.6047 21.6554 13.3908 22.0746 14.0797 22.0746C14.7685 22.0746 15.5097 21.7901 16.3033 21.2211C17.1119 20.6521 17.5162 20.0831 17.5162 19.5141C17.5162 19.0049 17.0146 18.4134 16.0113 17.7396C15.5921 17.4551 15.1728 17.1481 14.7535 16.8187C14.3492 16.4893 13.9973 16.07 13.6978 15.5609C13.4133 15.0517 13.271 14.4378 13.271 13.719C13.271 13.0003 13.6304 12.0045 14.3492 10.7317C15.068 9.44387 15.7268 8.74757 16.3258 8.64275C16.9248 8.53793 18.0029 8.48552 19.5602 8.48552Z', +}; + +export const CounterNote = ({ + index, + label, + width, + color = 'currentColor', + animationDelay = 200, +}: CounterNoteProps) => { + return ( +
+
+ + + + +
+ +
+ {label} +
+
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/page-link.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/page-link.tsx new file mode 100644 index 0000000000..fb36cb3922 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/page-link.tsx @@ -0,0 +1,15 @@ +import { LinkedPageIcon } from '@blocksuite/icons'; +import type { PropsWithChildren } from 'react'; + +import { pageLink, pageLinkIcon, pageLinkLabel } from '../articles/blocks.css'; + +interface PageLinkProps extends PropsWithChildren {} + +export const PageLink = ({ children }: PageLinkProps) => { + return ( + + + {children} + + ); +}; diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/shadow-sticker.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/shadow-sticker.tsx new file mode 100644 index 0000000000..c96b9587ef --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/shadow-sticker.tsx @@ -0,0 +1,29 @@ +import type { PropsWithChildren } from 'react'; + +import { shadowSticker } from './style.css'; + +interface ShadowStickerProps extends PropsWithChildren { + color?: string; + width?: number; + animate?: boolean; +} + +export const ShadowSticker = ({ + color = '#F9E8FF', + width, + animate = true, + children, +}: ShadowStickerProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/style.css.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/style.css.tsx new file mode 100644 index 0000000000..d01bccc2ae --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/style.css.tsx @@ -0,0 +1,149 @@ +import { keyframes, style } from '@vanilla-extract/css'; + +import { block } from '../articles/blocks.css'; +import { onboardingVars } from '../style.css'; + +export const switchButtons = style({ + display: 'flex', + alignItems: 'center', + gap: '16px', + padding: '8px', + borderRadius: '24px', + background: '#F4F4F5', + height: '64px', + width: '128px', + justifyContent: 'space-between', + + selectors: { + // indicator + '&::after': { + content: '', + width: '48px', + height: '48px', + borderRadius: '16px', + backgroundColor: 'white', + position: 'absolute', + transition: 'transform 0.15s ease', + boxShadow: 'var(--affine-shadow-1)', + }, + '&[data-mode="edgeless"]::after': { + transform: `translateX(64px)`, + }, + }, +}); + +export const switchButton = style({ + transform: 'scale(2)', + boxShadow: 'none', + opacity: 0.6, + selectors: { + '&:nth-child(1)': { + transformOrigin: 'left', + }, + '&:nth-child(2)': { + transformOrigin: 'right', + }, + '&[data-active="true"]': { + opacity: 1, + }, + }, +}); + +const pop = keyframes({ + from: { transform: 'translateY(100%)' }, + to: { transform: 'translateY(0)' }, +}); +export const toolbar = style({ + cursor: 'not-allowed', + boxShadow: '0px 0px 12px 0px #4241492E', + borderRadius: '16px', + border: `1px solid ${onboardingVars.toolbar.borderColor}`, + height: '65px', + overflow: 'hidden', + backgroundColor: onboardingVars.toolbar.bg, +}); +export const toolbarPop = style({ + vars: { + '--delay': '0s', + }, + + selectors: { + '[data-mode="edgeless"] &': { + transform: 'translateY(120%)', + animation: `${pop} 0.4s cubic-bezier(.04,1.01,.42,1.31) forwards`, + animationDelay: 'var(--delay)', + }, + }, +}); + +export const onboardingBlock = style([ + block, + { + vars: { + '--enter-delay': '0ms', + '--leave-delay': '0ms', + }, + padding: '0 28px', + cursor: 'unset', + borderRadius: '8px', + display: 'flex', + flexDirection: 'column', + gap: '16px', + marginBottom: '16px', + border: '2px solid transparent', + + selectors: { + '&[data-bg-mode="true"]': { + background: 'var(--bg)', + // boxShadow: 'var(--affine-menu-shadow)', // dark-mode issue + boxShadow: + '0px 0px 12px 0px rgba(66, 65, 73, 0.14), 0px 0px 0px 0.5px #E3E3E4 inset', + padding: '18px 28px', + borderColor: 'rgba(0, 0, 0, 0.1)', + }, + '&[data-invisible="true"]': { + opacity: 0, + pointerEvents: 'none', + marginBottom: 0, + }, + '&:last-child': { + marginBottom: 0, + }, + '&[data-mode="edgeless"]': { + transition: `all ${onboardingVars.block.transition} var(--enter-delay)`, + }, + '&[data-mode="page"]': { + transition: `all ${onboardingVars.block.transition} var(--leave-delay)`, + }, + }, + }, +]); + +export const shadowSticker = style({ + position: 'relative', + borderRadius: '8px', + boxShadow: '10px 10px 0px 6px #000', + padding: '22px 24px', + + fontSize: '15px', + lineHeight: '23px', + + selectors: { + '&::before': { + content: '""', + position: 'absolute', + inset: 0, + pointerEvents: 'none', + borderRadius: 'inherit', + boxShadow: '0px 0px 0px 6px #000', + }, + // use data-mode to apply animation only for edgeless mode + // this is a hacky way to do it + '[data-mode=edgeless] &[data-animate=true]': { + animation: `${keyframes({ + from: { boxShadow: '0px 0px 0px 0px #000' }, + to: { boxShadow: '10px 10px 0px 6px #000' }, + })} 0.6s ease forwards`, + }, + }, +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/switch.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/switch.tsx new file mode 100644 index 0000000000..49d2012297 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/switch.tsx @@ -0,0 +1,44 @@ +import clsx from 'clsx'; +import type { HTMLAttributes } from 'react'; + +import { + EdgelessSwitchItem, + PageSwitchItem, +} from '../../../blocksuite/block-suite-mode-switch/switch-items'; +import type { EdgelessSwitchMode } from '../types'; +import * as styles from './style.css'; + +interface EdgelessSwitchProps extends HTMLAttributes { + mode: EdgelessSwitchMode; + onSwitchToPageMode: () => void; + onSwitchToEdgelessMode: () => void; +} + +export const EdgelessSwitchButtons = ({ + mode, + className, + onSwitchToPageMode, + onSwitchToEdgelessMode, + ...attrs +}: EdgelessSwitchProps) => { + return ( +
+ + +
+ ); +}; diff --git a/packages/frontend/core/src/components/affine/onboarding/switch-widgets/toolbar.tsx b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/toolbar.tsx new file mode 100644 index 0000000000..67f338f714 --- /dev/null +++ b/packages/frontend/core/src/components/affine/onboarding/switch-widgets/toolbar.tsx @@ -0,0 +1,3743 @@ +import { type CSSProperties, memo } from 'react'; + +import { toolbar, toolbarPop } from './style.css'; + +export const ToolbarSVG = memo(function ToolbarSVG() { + const step = 0.04; + let v = 0; + const delay = () => ({ + style: { '--delay': `${(v += step)}s` } as CSSProperties, + }); + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}); diff --git a/packages/frontend/core/src/components/affine/onboarding/types.ts b/packages/frontend/core/src/components/affine/onboarding/types.ts index 500b2a4363..eb749c858c 100644 --- a/packages/frontend/core/src/components/affine/onboarding/types.ts +++ b/packages/frontend/core/src/components/affine/onboarding/types.ts @@ -1,7 +1,9 @@ -import type { ReactNode } from 'react'; +import type { CSSProperties } from '@vanilla-extract/css'; +import type { PropsWithChildren, ReactNode } from 'react'; -export type OnboardingStep = 'enter' | 'unfold' | 'mode-switch'; +export type OnboardingStep = 'enter' | 'unfold' | 'edgeless-switch'; export type ArticleId = '0' | '1' | '2' | '3' | '4'; +export type EdgelessSwitchMode = 'edgeless' | 'page'; /** * Paper enter animation options @@ -44,4 +46,53 @@ export interface ArticleOption { /** offset Y */ y: number; }; + + /** content that contains edgeless info */ + blocks: OnboardingBlockOption[]; + + /** apply an initial state for edgeless mode */ + initState?: EdgelessSwitchState; +} + +export interface OnboardingBlockOption extends PropsWithChildren { + /** + * if set, will apply special background styled for edgeless mode + */ + bg?: string; + /** + * only show in edgeless mode + */ + edgelessOnly?: boolean; + /** apply transform */ + offset?: { x?: number; y?: number }; + /** apply absolute position for edgeless mode */ + position?: { x?: number; y?: number }; + /** apply absolute position for page mode */ + fromPosition?: { x?: number; y?: number }; + /** enter delay in ms */ + enterDelay?: number; + /** leave delay in ms */ + leaveDelay?: number; + + style?: CSSProperties; + + /** customize style for different mode */ + customStyle?: { + page?: CSSProperties; + edgeless?: CSSProperties; + }; + + /** attach a sub block to current block */ + sub?: OnboardingBlockOption; +} + +export interface EdgelessSwitchState { + scale: number; + offsetX: number; + offsetY: number; +} + +export interface OnboardingStatus { + activeId: ArticleId | null; + unfoldingId: ArticleId | null; } diff --git a/packages/frontend/core/src/types/types.d.ts b/packages/frontend/core/src/types/types.d.ts index 59513b2af9..872d98d57f 100644 --- a/packages/frontend/core/src/types/types.d.ts +++ b/packages/frontend/core/src/types/types.d.ts @@ -13,3 +13,13 @@ declare module '*.assets.svg' { const url: string; export default url; } + +declare module '*.png' { + const url: string; + export default url; +} + +declare module '*.jpg' { + const url: string; + export default url; +} diff --git a/tests/storybook/src/stories/onboarding.stories.tsx b/tests/storybook/src/stories/onboarding.stories.tsx new file mode 100644 index 0000000000..4b75b6851f --- /dev/null +++ b/tests/storybook/src/stories/onboarding.stories.tsx @@ -0,0 +1,14 @@ +import type { Input } from '@affine/component'; +import { Onboarding } from '@affine/core/components/affine/onboarding/onboarding'; +import type { Meta, StoryFn } from '@storybook/react'; + +export default { + title: 'Preview/Onboarding', + parameters: { + chromatic: { disableSnapshot: true }, + }, +} satisfies Meta; + +export const Preview: StoryFn = () => { + return ; +};