refactor: workspace header (#1880)

This commit is contained in:
Himself65
2023-04-11 21:39:39 -05:00
committed by GitHub
parent 2e823c2fee
commit a06113d48c
42 changed files with 1653 additions and 470 deletions
@@ -0,0 +1,58 @@
import { atom, useAtomValue } from 'jotai';
import type { FC } from 'react';
import { useEffect, useRef } from 'react';
const lottieAtom = atom(async () => import('lottie-web').then(m => m.default));
type CustomLottieProps = {
options: {
loop?: boolean | number | undefined;
autoplay?: boolean | undefined;
animationData: any;
rendererSettings?: {
preserveAspectRatio?: string | undefined;
};
};
isStopped?: boolean | undefined;
speed?: number | undefined;
width?: number | string | undefined;
height?: number | string | undefined;
};
const CustomLottie: FC<CustomLottieProps> = ({
options,
isStopped,
speed,
width,
height,
}) => {
const element = useRef<HTMLDivElement>(null);
const lottieInstance = useRef<any>();
const lottie = useAtomValue(lottieAtom);
useEffect(() => {
if (element.current) {
lottieInstance.current = lottie.loadAnimation({
...options,
container: element.current,
});
}
return () => {
lottieInstance.current?.destroy();
};
}, [lottie, options]);
useEffect(() => {
if (speed) {
lottieInstance.current?.setSpeed(speed);
}
if (isStopped) {
lottieInstance.current?.stop();
} else {
lottieInstance.current?.play();
}
}, [isStopped, speed]);
return <div ref={element} style={{ width, height }}></div>;
};
export default CustomLottie;
@@ -0,0 +1,669 @@
{
"v": "5.9.0",
"fr": 29.9700012207031,
"ip": 0,
"op": 60.0000024438501,
"w": 500,
"h": 500,
"nm": "edgeless-hover",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Layer 3/paper-edgeless-icons Outlines",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [250, 250, 0], "ix": 2, "l": 2 },
"a": { "a": 0, "k": [183.5, 183.5, 0], "ix": 1, "l": 2 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, -5.604],
[0, 0],
[-5.614, 0],
[0, 0],
[0, 5.605],
[0, 0],
[5.615, 0]
],
"o": [
[-5.614, 0],
[0, 0],
[0, 5.605],
[0, 0],
[5.615, 0],
[0, 0],
[0, -5.604],
[0, 0]
],
"v": [
[-30.525, -40.7],
[-40.699, -30.525],
[-40.699, 30.524],
[-30.525, 40.699],
[30.525, 40.699],
[40.699, 30.524],
[40.699, -30.525],
[30.525, -40.7]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[22.447, 0],
[0, 0],
[0, 22.437],
[0, 0],
[-22.446, 0],
[0, 0],
[0, -22.437],
[0, 0]
],
"o": [
[0, 0],
[-22.446, 0],
[0, 0],
[0, -22.437],
[0, 0],
[22.447, 0],
[0, 0],
[0, 22.437]
],
"v": [
[30.525, 71.224],
[-30.525, 71.224],
[-71.225, 30.524],
[-71.225, -30.525],
[-30.525, -71.225],
[30.525, -71.225],
[71.224, -30.525],
[71.224, 30.524]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [295.322, 295.323],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [315.322, 315.323],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [295.322, 295.323] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 1",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.426, 0],
[0, 8.426],
[0, 0],
[-8.426, 0],
[0, -8.426],
[0, 0]
],
"o": [
[-8.426, 0],
[0, 0],
[0, -8.426],
[8.426, 0],
[0, 0],
[0, 8.426]
],
"v": [
[0, 30.525],
[-15.262, 15.263],
[-15.262, -15.262],
[0, -30.525],
[15.262, -15.262],
[15.262, 15.263]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [76.561, 183.399],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [56.561, 183.399],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [76.561, 183.399] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 2",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 2,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.426, 0],
[0, 8.426],
[5.395, 13.057],
[9.956, 9.956],
[13.037, 5.406],
[14.1, 0],
[0, 8.426],
[-8.426, 0],
[-16.753, -6.955],
[-12.808, -12.818],
[-6.955, -16.773],
[0, -18.105]
],
"o": [
[-8.426, 0],
[0, -14.09],
[-5.416, -13.016],
[-9.957, -9.956],
[-13.026, -5.385],
[-8.426, 0],
[0, -8.426],
[18.134, 0],
[16.763, 6.936],
[12.788, 12.778],
[6.936, 16.773],
[0, 8.426]
],
"v": [
[61.049, 76.312],
[45.788, 61.05],
[37.66, 20.151],
[14.497, -14.487],
[-20.161, -37.659],
[-61.049, -45.787],
[-76.311, -61.049],
[-61.049, -76.312],
[-8.475, -65.839],
[36.09, -36.069],
[65.858, 8.486],
[76.311, 61.05]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [254.447, 112.349],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [274.447, 92.349],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [254.447, 112.349] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 3",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 3,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[22.446, 0],
[0, -22.437],
[-22.446, 0],
[0, 22.436]
],
"o": [
[-22.446, 0],
[0, 22.436],
[22.446, 0],
[0, -22.437]
],
"v": [
[0, -40.7],
[-40.7, 0],
[0, 40.699],
[40.7, 0]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[39.269, 0],
[0, 39.268],
[-39.269, 0],
[0, -39.269]
],
"o": [
[-39.269, 0],
[0, -39.269],
[39.269, 0],
[0, 39.268]
],
"v": [
[0, 71.224],
[-71.224, 0],
[0, -71.225],
[71.224, 0]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [71.474, 295.323],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [51.474, 315.323],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [71.474, 295.323] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 4",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 4,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[22.446, 0],
[0, -22.437],
[-22.446, 0],
[0, 22.436]
],
"o": [
[-22.446, 0],
[0, 22.436],
[22.446, 0],
[0, -22.437]
],
"v": [
[0, -40.7],
[-40.7, 0.001],
[0, 40.7],
[40.7, 0.001]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[39.269, 0],
[0, 39.268],
[-39.269, 0],
[0, -39.269]
],
"o": [
[-39.269, 0],
[0, -39.269],
[39.269, 0],
[0, 39.268]
],
"v": [
[0, 71.225],
[-71.224, 0.001],
[0, -71.225],
[71.224, 0.001]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [71.474, 71.475],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [51.474, 51.475],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [71.474, 71.475] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 5",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 5,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 60.0000024438501,
"st": 0,
"bm": 0
}
],
"markers": []
}
@@ -0,0 +1,521 @@
{
"v": "5.9.0",
"fr": 29.9700012207031,
"ip": 0,
"op": 60.0000024438501,
"w": 500,
"h": 500,
"nm": "page-hover",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Layer 1/paper-edgeless-icons Outlines",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [250, 250, 0], "ix": 2, "l": 2 },
"a": { "a": 0, "k": [183, 183, 0], "ix": 1, "l": 2 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[22.557, 0],
[0, 0],
[0, 8.639],
[-8.64, 0],
[0, 0],
[-2.201, 1.651],
[0, 9.536],
[0, 0],
[-8.64, 0],
[0, -8.64],
[0, 0],
[16.892, -16.892]
],
"o": [
[0, 0],
[-8.64, 0],
[0, -8.64],
[0, 0],
[17.442, 0],
[10.412, -10.453],
[0, 0],
[0, -8.64],
[8.639, 0],
[0, 0],
[0, 18.155],
[-9.027, 8.987]
],
"v": [
[46.947, 182.579],
[-109.545, 182.579],
[-125.194, 166.93],
[-109.545, 151.281],
[46.947, 151.281],
[78.001, 145.086],
[93.895, 114.766],
[93.895, -166.93],
[109.545, -182.579],
[125.194, -166.93],
[125.194, 114.766],
[99.743, 167.561]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [240.204, 182.829],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [260.204, 202.829],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [240.204, 182.829] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 1",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.64, 0],
[0, 8.639],
[0, 0],
[-16.892, 16.882],
[-22.598, 0],
[0, 0],
[0, -8.64],
[8.639, 0],
[0, 0],
[2.171, -1.65],
[0, -9.546],
[0, 0]
],
"o": [
[-8.64, 0],
[0, 0],
[0, -18.145],
[8.976, -8.986],
[0, 0],
[8.639, 0],
[0, 8.64],
[0, 0],
[-17.453, 0],
[-10.443, 10.484],
[0, 0],
[0, 8.639]
],
"v": [
[-109.545, 182.579],
[-125.194, 166.93],
[-125.194, -114.766],
[-99.743, -167.562],
[-46.948, -182.579],
[109.545, -182.579],
[125.194, -166.93],
[109.545, -151.281],
[-46.948, -151.281],
[-77.972, -145.107],
[-93.896, -114.766],
[-93.896, 166.93]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [125.443, 182.829],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [105.443, 162.829],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [125.443, 182.829] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 2",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 2,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[8.64, 0],
[0, 0],
[0, 8.639],
[-8.64, 0],
[0, 0],
[0, -8.64]
],
"o": [
[0, 0],
[-8.64, 0],
[0, -8.64],
[0, 0],
[8.64, 0],
[0, 8.639]
],
"v": [
[67.813, 15.649],
[-67.813, 15.649],
[-83.463, 0],
[-67.813, -15.649],
[67.813, -15.649],
[83.462, 0]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [182.824, 271.513],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [212.824, 281.513],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [182.824, 271.513] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 3",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 3,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, -5.757],
[0, 0],
[-5.756, 0],
[0, 0],
[0, 5.756],
[0, 0],
[5.747, 0]
],
"o": [
[-5.756, 0],
[0, 0],
[0, 5.756],
[0, 0],
[5.747, 0],
[0, 0],
[0, -5.757],
[0, 0]
],
"v": [
[-41.732, -31.294],
[-52.165, -20.86],
[-52.165, 20.87],
[-41.732, 31.303],
[41.73, 31.303],
[52.163, 20.87],
[52.163, -20.86],
[41.73, -31.294]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ind": 1,
"ty": "sh",
"ix": 2,
"ks": {
"a": 0,
"k": {
"i": [
[23.006, 0],
[0, 0],
[0, 23.015],
[0, 0],
[-23.015, 0],
[0, 0],
[0, -23.016],
[0, 0]
],
"o": [
[0, 0],
[-23.015, 0],
[0, 0],
[0, -23.016],
[0, 0],
[23.006, 0],
[0, 0],
[0, 23.015]
],
"v": [
[41.73, 62.592],
[-41.732, 62.592],
[-83.463, 20.87],
[-83.463, -20.86],
[-41.732, -62.592],
[41.73, -62.592],
[83.463, -20.86],
[83.463, 20.87]
],
"c": true
},
"ix": 2
},
"nm": "Path 2",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "mm",
"mm": 1,
"nm": "Merge Paths 1",
"mn": "ADBE Vector Filter - Merge",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 0,
"s": [182.824, 141.088],
"to": [0, 0],
"ti": [0, 0]
},
{
"i": { "x": 0.833, "y": 0.833 },
"o": { "x": 0.167, "y": 0.167 },
"t": 30,
"s": [152.824, 131.088],
"to": [0, 0],
"ti": [0, 0]
},
{ "t": 58.0000023623884, "s": [182.824, 141.088] }
],
"ix": 2
},
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 4",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 4,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 60.0000024438501,
"st": 0,
"bm": 0
}
],
"markers": []
}
@@ -0,0 +1,59 @@
import { assertExists } from '@blocksuite/store';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { usePageMeta } from '../../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { StyledEditorModeSwitch } from './style';
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
export type EditorModeSwitchProps = {
// todo(himself65): combine these two properties
blockSuiteWorkspace: BlockSuiteWorkspace;
pageId: string;
style?: CSSProperties;
};
export const EditorModeSwitch = ({
style,
blockSuiteWorkspace,
pageId,
}: EditorModeSwitchProps) => {
const currentMode =
useAtomValue(workspacePreferredModeAtom)[pageId] ?? 'page';
const setMode = useSetAtom(workspacePreferredModeAtom);
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
assertExists(pageMeta);
const { trash } = pageMeta;
return (
<StyledEditorModeSwitch
style={style}
switchLeft={currentMode === 'page'}
showAlone={trash}
>
<PageSwitchItem
data-testid="switch-page-mode-button"
active={currentMode === 'page'}
hide={trash && currentMode !== 'page'}
onClick={() => {
setMode(mode => ({ ...mode, [pageMeta.id]: 'page' }));
toast('Page mode');
}}
/>
<EdgelessSwitchItem
data-testid="switch-edgeless-mode-button"
active={currentMode === 'edgeless'}
hide={trash && currentMode !== 'edgeless'}
onClick={() => {
setMode(mode => ({ ...mode, [pageMeta.id]: 'edgeless' }));
toast('Edgeless mode');
}}
/>
</StyledEditorModeSwitch>
);
};
@@ -0,0 +1,61 @@
import { displayFlex, styled } from '@affine/component';
export const StyledEditorModeSwitch = styled('div')<{
switchLeft: boolean;
showAlone?: boolean;
}>(({ theme, switchLeft, showAlone }) => {
const {
palette: { mode },
} = theme;
return {
width: showAlone ? '40px' : '78px',
height: '32px',
background: showAlone
? 'transparent'
: mode === 'dark'
? '#242424'
: '#F9F9FB',
borderRadius: '12px',
...displayFlex('space-between', 'center'),
padding: '0 8px',
position: 'relative',
'::after': {
content: '""',
display: showAlone ? 'none' : 'block',
width: '24px',
height: '24px',
background: theme.colors.pageBackground,
boxShadow:
mode === 'dark'
? '0px 0px 6px rgba(22, 22, 22, 0.6)'
: '0px 0px 6px #E2E2E2',
borderRadius: '8px',
zIndex: 1,
position: 'absolute',
transform: `translateX(${switchLeft ? '0' : '38px'})`,
transition: 'all .15s',
},
};
});
export const StyledSwitchItem = styled('button')<{
active?: boolean;
hide?: boolean;
}>(({ theme, active = false, hide = false }) => {
return {
width: '24px',
height: '24px',
borderRadius: '8px',
color: active ? theme.colors.primaryColor : theme.colors.iconColor,
display: hide ? 'none' : 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
zIndex: 2,
fontSize: '20px',
path: {
fill: 'currentColor',
},
};
});
@@ -0,0 +1,79 @@
import type { HTMLAttributes } from 'react';
import type React from 'react';
import { cloneElement, useState } from 'react';
import Lottie from './CustomLottie';
import { StyledSwitchItem } from './style';
type HoverAnimateControllerProps = {
active?: boolean;
hide?: boolean;
children: React.ReactElement;
} & HTMLAttributes<HTMLButtonElement>;
const HoverAnimateController = ({
active,
hide,
children,
...props
}: HoverAnimateControllerProps) => {
const [startAnimate, setStartAnimate] = useState(false);
return (
<StyledSwitchItem
hide={hide}
active={active}
onMouseEnter={() => {
setStartAnimate(true);
}}
onMouseLeave={() => {
setStartAnimate(false);
}}
{...props}
>
{cloneElement(children, {
isStopped: !startAnimate,
speed: 5,
width: 20,
height: 20,
})}
</StyledSwitchItem>
);
};
export const PageSwitchItem = (
props: Omit<HoverAnimateControllerProps, 'children'>
) => {
return (
<HoverAnimateController {...props}>
<Lottie
options={{
loop: false,
autoplay: false,
animationData: require('./animation-data/page-hover.json'),
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
}}
/>
</HoverAnimateController>
);
};
export const EdgelessSwitchItem = (
props: Omit<HoverAnimateControllerProps, 'children'>
) => {
return (
<HoverAnimateController {...props}>
<Lottie
options={{
loop: false,
autoplay: false,
animationData: require('./animation-data/edgeless-hover.json'),
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
}}
/>
</HoverAnimateController>
);
};
@@ -0,0 +1,134 @@
// fixme(himself65): refactor this file
import { FlexWrapper, IconButton, Menu, MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
EdgelessIcon,
FavoritedIcon,
FavoriteIcon,
MoreVerticalIcon,
PageIcon,
} from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useTheme } from '@mui/material';
import { useAtom } from 'jotai';
import { useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import {
usePageMeta,
usePageMetaHelper,
} from '../../../../hooks/use-page-meta';
import { toast } from '../../../../utils';
import {
Export,
MoveTo,
MoveToTrash,
} from '../../../affine/operation-menu-items';
export const EditorOptionMenu = () => {
const { t } = useTranslation();
const theme = useTheme();
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
const [pageId] = useCurrentPageId();
assertExists(workspace);
assertExists(pageId);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
const allMetas = usePageMeta(blockSuiteWorkspace);
const [record, set] = useAtom(workspacePreferredModeAtom);
const mode = record[pageId] ?? 'page';
assertExists(pageMeta);
const { favorite } = pageMeta;
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [openConfirm, setOpenConfirm] = useState(false);
const EditMenu = (
<>
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={() => {
setPageMeta(pageId, { favorite: !favorite });
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
icon={
favorite ? (
<FavoritedIcon style={{ color: theme.colors.primaryColor }} />
) : (
<FavoriteIcon />
)
}
>
{favorite ? t('Remove from favorites') : t('Add to Favorites')}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={() => {
set(record => ({
...record,
[pageId]: mode === 'page' ? 'edgeless' : 'page',
}));
}}
>
{t('Convert to ')}
{mode === 'page' ? t('Edgeless') : t('Page')}
</MenuItem>
<Export />
{!pageMeta.isRootPinboard && (
<MoveTo
metas={allMetas}
currentMeta={pageMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
)}
{!pageMeta.isRootPinboard && (
<MoveToTrash
testId="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
)}
</>
);
return (
<>
<FlexWrapper alignItems="center" justifyContent="center">
<Menu
width={276}
content={EditMenu}
placement="bottom-end"
disablePortal={true}
trigger="click"
>
<IconButton data-testid="editor-option-menu" iconSize={[24, 24]}>
<MoreVerticalIcon />
</IconButton>
</Menu>
<MoveToTrash.ConfirmModal
open={openConfirm}
meta={pageMeta}
onConfirm={() => {
toast(t('Moved to Trash'));
setPageMeta(pageMeta.id, {
trash: true,
trashDate: +new Date(),
});
}}
onCancel={() => {
setOpenConfirm(false);
}}
/>
</FlexWrapper>
</>
);
};
@@ -0,0 +1,161 @@
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
getLoginStorage,
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
CloudWorkspaceIcon,
LocalWorkspaceIcon,
NoNetworkIcon,
} from '@blocksuite/icons';
import { assertEquals, assertExists } from '@blocksuite/store';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
import { affineAuth } from '../../../../plugins/affine';
import type { AffineOfficialWorkspace } from '../../../../shared';
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
const IconWrapper = styled('div')(({ theme }) => {
return {
width: '32px',
height: '32px',
marginRight: '12px',
fontSize: '24px',
color: theme.colors.iconColor,
...displayFlex('center', 'center'),
};
});
const getStatus = (workspace: AffineOfficialWorkspace) => {
if (!navigator.onLine) {
return 'offline';
}
if (workspace.flavour === 'local') {
return 'local';
}
return 'cloud';
};
export const SyncUser = () => {
//#region fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
assertExists(workspace);
const router = useRouter();
const [status, setStatus] = useState<'offline' | 'local' | 'cloud'>(
getStatus(workspace)
);
const [prevWorkspace, setPrevWorkspace] = useState(workspace);
if (prevWorkspace !== workspace) {
setPrevWorkspace(workspace);
setStatus(getStatus(workspace));
}
useEffect(() => {
const online = () => {
setStatus(getStatus(workspace));
};
const offline = () => {
setStatus('offline');
};
window.addEventListener('online', online);
window.addEventListener('offline', offline);
return () => {
window.removeEventListener('online', online);
window.removeEventListener('offline', offline);
};
}, [workspace]);
//#endregion
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const transformWorkspace = useTransformWorkspace();
if (status === 'offline') {
return (
<Tooltip
content={t('Please make sure you are online')}
placement="bottom-end"
>
<IconWrapper>
<NoNetworkIcon />
</IconWrapper>
</Tooltip>
);
}
if (status === 'local') {
return (
<>
<Tooltip
content={t('Saved then enable AFFiNE Cloud')}
placement="bottom-end"
>
<IconButton
onClick={() => {
setOpen(true);
}}
style={{ marginRight: '12px' }}
>
<LocalWorkspaceIcon />
</IconButton>
</Tooltip>
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {
setOpen(false);
}}
onConform={async () => {
if (!getLoginStorage()) {
const response = await affineAuth.generateToken(
SignMethod.Google
);
if (response) {
setLoginStorage(response);
}
router.reload();
return;
}
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
const id = await transformWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
workspace as LocalWorkspace
);
// fixme(himself65): refactor this
router
.replace({
pathname: `/workspace/[workspaceId]/all`,
query: {
workspaceId: id,
},
})
.then(() => {
router.reload();
});
setOpen(false);
}}
/>
</>
);
}
return (
<Tooltip content={t('Synced with AFFiNE Cloud')} placement="bottom-end">
<IconWrapper>
<CloudWorkspaceIcon />
</IconWrapper>
</Tooltip>
);
};
export default SyncUser;
@@ -0,0 +1,83 @@
import { Button, Confirm } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { assertExists } from '@blocksuite/store';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import {
usePageMeta,
usePageMetaHelper,
} from '../../../../hooks/use-page-meta';
export const TrashButtonGroup = () => {
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
const [pageId] = useCurrentPageId();
assertExists(workspace);
assertExists(pageId);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
assertExists(pageMeta);
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const router = useRouter();
//
const [open, setOpen] = useState(false);
const { t } = useTranslation();
return (
<>
<Button
bold={true}
shape="round"
style={{ marginRight: '24px' }}
onClick={() => {
setPageMeta(pageId, { trash: false });
}}
>
{t('Restore it')}
</Button>
<Button
bold={true}
shape="round"
type="danger"
onClick={() => {
setOpen(true);
}}
>
{t('Delete permanently')}
</Button>
<Confirm
title={t('TrashButtonGroupTitle')}
content={t('TrashButtonGroupDescription')}
confirmText={t('Delete')}
confirmType="danger"
open={open}
onConfirm={() => {
// fixme(himself65): remove these hooks ASAP
router
.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: workspace.id,
},
})
.then(() => {
blockSuiteWorkspace.removePage(pageId);
});
}}
onCancel={() => {
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
/>
</>
);
};
export default TrashButtonGroup;
@@ -0,0 +1,44 @@
import type { CSSProperties, DOMAttributes } from 'react';
type IconProps = {
style?: CSSProperties;
} & DOMAttributes<SVGElement>;
export const MoonIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
style={style}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.54893 3.31407C9.33328 3.08158 9.27962 2.74521 9.41255 2.45912C9.54547 2.17302 9.83936 1.99233 10.1595 1.99986C13.4456 2.07712 16.5114 4.08044 17.7359 7.29071C19.3437 11.5057 17.1672 16.2024 12.8744 17.781C9.60251 18.9843 6.04745 18.0285 3.82974 15.6428C3.61375 15.4104 3.55978 15.0739 3.69257 14.7876C3.82537 14.5014 4.11931 14.3205 4.43962 14.3279C5.27228 14.3474 6.12412 14.2171 6.94979 13.9135C10.415 12.6391 12.172 8.84782 10.8741 5.44537C10.5657 4.63692 10.1061 3.91474 9.54893 3.31407Z"
/>
</svg>
);
};
export const SunIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
style={style}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.8002 2.5002C10.8002 2.05837 10.442 1.7002 10.0002 1.7002C9.55837 1.7002 9.2002 2.05837 9.2002 2.5002V3.33353C9.2002 3.77536 9.55837 4.13353 10.0002 4.13353C10.442 4.13353 10.8002 3.77536 10.8002 3.33353V2.5002ZM5.14921 4.01784C4.83679 3.70542 4.33026 3.70542 4.01784 4.01784C3.70542 4.33026 3.70542 4.83679 4.01784 5.14921L4.69627 5.82764C5.00869 6.14006 5.51522 6.14006 5.82764 5.82764C6.14006 5.51522 6.14006 5.00869 5.82764 4.69627L5.14921 4.01784ZM15.9825 5.1492C16.2949 4.83678 16.2949 4.33025 15.9825 4.01783C15.6701 3.70542 15.1636 3.70543 14.8511 4.01785L14.1727 4.69628C13.8603 5.00871 13.8603 5.51524 14.1728 5.82765C14.4852 6.14007 14.9917 6.14006 15.3041 5.82763L15.9825 5.1492ZM10.0002 5.86686C7.71742 5.86686 5.86686 7.71742 5.86686 10.0002C5.86686 12.283 7.71742 14.1335 10.0002 14.1335C12.283 14.1335 14.1335 12.283 14.1335 10.0002C14.1335 7.71742 12.283 5.86686 10.0002 5.86686ZM2.5002 9.2002C2.05837 9.2002 1.7002 9.55837 1.7002 10.0002C1.7002 10.442 2.05837 10.8002 2.5002 10.8002H3.33353C3.77536 10.8002 4.13353 10.442 4.13353 10.0002C4.13353 9.55837 3.77536 9.2002 3.33353 9.2002H2.5002ZM16.6669 9.2002C16.225 9.2002 15.8669 9.55837 15.8669 10.0002C15.8669 10.442 16.225 10.8002 16.6669 10.8002H17.5002C17.942 10.8002 18.3002 10.442 18.3002 10.0002C18.3002 9.55837 17.942 9.2002 17.5002 9.2002H16.6669ZM5.82623 15.309C6.13943 14.9973 6.14069 14.4908 5.82906 14.1776C5.51742 13.8644 5.01089 13.8631 4.69769 14.1748L4.01926 14.8498C3.70606 15.1615 3.70479 15.668 4.01643 15.9812C4.32807 16.2944 4.8346 16.2956 5.1478 15.984L5.82623 15.309ZM15.3027 14.1748C14.9895 13.8631 14.483 13.8644 14.1713 14.1776C13.8597 14.4908 13.861 14.9973 14.1742 15.3089L14.8526 15.984C15.1658 16.2956 15.6723 16.2944 15.9839 15.9812C16.2956 15.668 16.2943 15.1615 15.9811 14.8498L15.3027 14.1748ZM10.8002 16.6669C10.8002 16.225 10.442 15.8669 10.0002 15.8669C9.55837 15.8669 9.2002 16.225 9.2002 16.6669V17.5002C9.2002 17.942 9.55837 18.3002 10.0002 18.3002C10.442 18.3002 10.8002 17.942 10.8002 17.5002V16.6669Z"
/>
</svg>
);
};
@@ -0,0 +1,43 @@
import { DarkModeIcon, LightModeIcon } from '@blocksuite/icons';
import { useTheme } from 'next-themes';
import { useState } from 'react';
import { StyledSwitchItem, StyledThemeModeSwitch } from './style';
export const ThemeModeSwitch = () => {
const { setTheme, resolvedTheme } = useTheme();
const [isHover, setIsHover] = useState(false);
return (
<StyledThemeModeSwitch
data-testid="change-theme-container"
onMouseEnter={() => {
setIsHover(true);
}}
onMouseLeave={() => {
setIsHover(false);
}}
>
<StyledSwitchItem
data-testid="change-theme-light"
active={resolvedTheme === 'light'}
isHover={isHover}
onClick={() => {
setTheme('light');
}}
>
<LightModeIcon />
</StyledSwitchItem>
<StyledSwitchItem
data-testid="change-theme-dark"
active={resolvedTheme === 'dark'}
isHover={isHover}
onClick={() => {
setTheme('dark');
}}
>
<DarkModeIcon />
</StyledSwitchItem>
</StyledThemeModeSwitch>
);
};
export default ThemeModeSwitch;
@@ -0,0 +1,75 @@
import { css, displayFlex, keyframes, styled } from '@affine/component';
// @ts-ignore
import spring, { toString } from 'css-spring';
const ANIMATE_DURATION = 400;
export const StyledThemeModeSwitch = styled('button')(({ theme }) => {
return {
width: '32px',
height: '32px',
borderRadius: '6px',
overflow: 'hidden',
backgroundColor: 'transparent',
position: 'relative',
color: theme.colors.iconColor,
fontSize: '24px',
};
});
export const StyledSwitchItem = styled('div')<{
active: boolean;
isHover: boolean;
}>(({ active, isHover, theme }) => {
const activeRaiseAnimate = toString(
spring({ top: '0' }, { top: '-100%' }, { preset: 'gentle' })
);
const raiseAnimate = toString(
spring({ top: '100%' }, { top: '0' }, { preset: 'gentle' })
);
const activeDeclineAnimate = toString(
spring({ top: '-100%' }, { top: '0' }, { preset: 'gentle' })
);
const declineAnimate = toString(
spring({ top: '0' }, { top: '100%' }, { preset: 'gentle' })
);
const activeStyle = active
? {
color: theme.colors.iconColor,
top: '0',
animation: css`
${keyframes`${
isHover ? activeRaiseAnimate : activeDeclineAnimate
}`} ${ANIMATE_DURATION}ms forwards
`,
animationDirection: isHover ? 'normal' : 'alternate',
}
: {
top: '100%',
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
animation: css`
${keyframes`${
isHover ? raiseAnimate : declineAnimate
}`} ${ANIMATE_DURATION}ms forwards
`,
animationDirection: isHover ? 'normal' : 'alternate',
};
return css`
${css(displayFlex('center', 'center'))}
width: 32px;
height: 32px;
position: absolute;
left: 0;
cursor: pointer;
color: ${activeStyle.color}
top: ${activeStyle.top};
background-color: ${activeStyle.backgroundColor};
animation: ${activeStyle.animation};
animation-direction: ${activeStyle.animationDirection};
//svg {
// width: 24px;
// height: 24px;
//},
`;
});
@@ -0,0 +1,177 @@
import { useTranslation } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import type React from 'react';
import { forwardRef, useEffect, useMemo, useState } from 'react';
import {
useSidebarFloating,
useSidebarStatus,
} from '../../../hooks/use-sidebar-status';
import type { AffineOfficialWorkspace } from '../../../shared';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
import SyncUser from './header-right-items/SyncUser';
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import {
StyledBrowserWarning,
StyledCloseButton,
StyledHeader,
StyledHeaderContainer,
StyledHeaderRightSide,
} from './styles';
import { OSWarningMessage, shouldShowWarning } from './utils';
const BrowserWarning = ({
show,
onClose,
}: {
show: boolean;
onClose: () => void;
}) => {
return (
<StyledBrowserWarning show={show}>
<OSWarningMessage />
<StyledCloseButton onClick={onClose}>
<CloseIcon />
</StyledCloseButton>
</StyledBrowserWarning>
);
};
export type BaseHeaderProps<
Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace
> = {
workspace: Workspace;
currentPage: Page | null;
isPublic: boolean;
isPreview: boolean;
};
export const enum HeaderRightItemName {
EditorOptionMenu = 'editorOptionMenu',
TrashButtonGroup = 'trashButtonGroup',
ThemeModeSwitch = 'themeModeSwitch',
SyncUser = 'syncUser',
ShareMenu = 'shareMenu',
}
type HeaderItem = {
Component: React.FC<BaseHeaderProps>;
// todo: public workspace should be one of the flavour
availableWhen: (
workspace: AffineOfficialWorkspace,
currentPage: Page | null,
status: {
isPublic: boolean;
isPreview: boolean;
}
) => boolean;
};
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
[HeaderRightItemName.TrashButtonGroup]: {
Component: TrashButtonGroup,
availableWhen: (_, currentPage) => {
return currentPage?.meta.trash === true;
},
},
[HeaderRightItemName.SyncUser]: {
Component: SyncUser,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return !isPublic && !isPreview;
},
},
[HeaderRightItemName.ThemeModeSwitch]: {
Component: ThemeModeSwitch,
availableWhen: (_, currentPage) => {
return currentPage?.meta.trash !== true;
},
},
[HeaderRightItemName.EditorOptionMenu]: {
Component: EditorOptionMenu,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return !!currentPage && !isPublic && !isPreview;
},
},
[HeaderRightItemName.ShareMenu]: {
Component: () => null,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return false;
},
},
};
export type HeaderProps = BaseHeaderProps;
export const Header = forwardRef<
HTMLDivElement,
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const [showWarning, setShowWarning] = useState(false);
useEffect(() => {
setShowWarning(shouldShowWarning());
}, []);
const [open] = useSidebarStatus();
const sidebarFloating = useSidebarFloating();
const { t } = useTranslation();
return (
<StyledHeaderContainer
sidebarFloating={sidebarFloating && open}
ref={ref}
hasWarning={showWarning}
{...props}
>
<BrowserWarning
show={showWarning}
onClose={() => {
setShowWarning(false);
}}
/>
<StyledHeader
hasWarning={showWarning}
data-testid="editor-header-items"
data-tauri-drag-region
>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
testid="sliderBar-arrowButton-expand"
/>
{props.children}
<StyledHeaderRightSide>
{useMemo(() => {
return Object.entries(HeaderRightItems).map(
([name, { availableWhen, Component }]) => {
if (
availableWhen(props.workspace, props.currentPage, {
isPreview: props.isPreview,
isPublic: props.isPublic,
})
) {
return (
<Component
workspace={props.workspace}
currentPage={props.currentPage}
isPreview={props.isPreview}
isPublic={props.isPublic}
key={name}
/>
);
}
return null;
}
);
}, [props])}
{/*<ShareMenu />*/}
</StyledHeaderRightSide>
</StyledHeader>
</StyledHeaderContainer>
);
});
Header.displayName = 'Header';
@@ -0,0 +1,128 @@
import type { PopperProps } from '@affine/component';
import { QuickSearchTips } from '@affine/component';
import { getEnvironment } from '@affine/env';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useAtomValue, useSetAtom } from 'jotai';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useRef } from 'react';
import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms';
import { useGuideHidden } from '../../../hooks/use-is-first-load';
import { usePageMeta } from '../../../hooks/use-page-meta';
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
import { QuickSearchButton } from '../../pure/quick-search-button';
import { EditorModeSwitch } from './editor-mode-switch';
import type { BaseHeaderProps } from './header';
import { Header } from './header';
import {
StyledQuickSearchTipButton,
StyledQuickSearchTipContent,
StyledSearchArrowWrapper,
StyledSwitchWrapper,
StyledTitle,
StyledTitleContainer,
StyledTitleWrapper,
} from './styles';
export type WorkspaceHeaderProps = BaseHeaderProps;
export const WorkspaceHeader = forwardRef<
HTMLDivElement,
PropsWithChildren<WorkspaceHeaderProps> & HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const { workspace, currentPage, children, isPublic } = props;
// fixme(himself65): remove this atom and move it to props
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
const pageMeta = usePageMeta(workspace.blockSuiteWorkspace).find(
meta => meta.id === currentPage?.id
);
assertExists(pageMeta);
const title = pageMeta.title;
const [isTipsHidden, setTipsHidden] = useGuideHidden();
const isMac = () => {
const env = getEnvironment();
return env.isBrowser && env.isMacOs;
};
const popperRef: PopperProps['popperRef'] = useRef(null);
useElementResizeEffect(
useAtomValue(currentEditorAtom),
useCallback(() => {
if (isTipsHidden.quickSearchTips || !popperRef.current) {
return;
}
popperRef.current.update();
}, [isTipsHidden.quickSearchTips])
);
const TipsContent = (
<StyledQuickSearchTipContent>
<div>
Click button
{
<span
style={{
fontSize: '24px',
verticalAlign: 'middle',
}}
>
<ArrowDownSmallIcon />
</span>
}
or use
{isMac() ? ' ⌘ + K' : ' Ctrl + K'} to activate Quick Search. Then you
can search keywords or quickly open recently viewed pages.
</div>
<StyledQuickSearchTipButton
data-testid="quick-search-got-it"
onClick={() =>
setTipsHidden({ ...isTipsHidden, quickSearchTips: true })
}
>
Got it
</StyledQuickSearchTipButton>
</StyledQuickSearchTipContent>
);
return (
<Header ref={ref} {...props}>
{children}
{!isPublic && currentPage && (
<StyledTitleContainer data-tauri-drag-region>
<StyledTitleWrapper>
<StyledSwitchWrapper>
<EditorModeSwitch
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
pageId={currentPage.id}
style={{
marginRight: '12px',
}}
/>
</StyledSwitchWrapper>
<StyledTitle>{title || 'Untitled'}</StyledTitle>
<QuickSearchTips
data-testid="quick-search-tips"
content={TipsContent}
placement="bottom"
popperRef={popperRef}
open={!isTipsHidden.quickSearchTips}
offset={[0, -5]}
>
<StyledSearchArrowWrapper>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}}
/>
</StyledSearchArrowWrapper>
</QuickSearchTips>
</StyledTitleWrapper>
</StyledTitleContainer>
)}
</Header>
);
});
WorkspaceHeader.displayName = 'BlockSuiteEditorHeader';
@@ -0,0 +1,170 @@
import {
absoluteCenter,
displayFlex,
styled,
textEllipsis,
} from '@affine/component';
export const StyledHeaderContainer = styled('div')<{
hasWarning: boolean;
sidebarFloating: boolean;
}>(({ theme, hasWarning, sidebarFloating }) => {
return {
height: hasWarning ? '96px' : '52px',
flexShrink: 0,
position: 'sticky',
top: 0,
background: theme.colors.pageBackground,
zIndex: 1,
WebkitAppRegion: sidebarFloating ? '' : 'drag',
button: {
WebkitAppRegion: 'no-drag',
},
};
});
export const StyledHeader = styled('div')<{ hasWarning: boolean }>(
({ theme }) => {
return {
flexShrink: 0,
height: '52px',
width: '100%',
padding: '0 20px',
...displayFlex('space-between', 'center'),
background: theme.colors.pageBackground,
zIndex: 99,
position: 'relative',
};
}
);
export const StyledTitleContainer = styled('div')(({ theme }) => ({
width: '100%',
height: '100%',
margin: 'auto',
...absoluteCenter({ horizontal: true, position: { top: 0 } }),
...displayFlex('center', 'center'),
fontSize: theme.font.base,
}));
export const StyledTitle = styled('div')(({ theme }) => {
return {
maxWidth: '620px',
[theme.breakpoints.down('lg')]: {
maxWidth: '480px',
},
[theme.breakpoints.down('md')]: {
maxWidth: '240px',
},
[theme.breakpoints.down('sm')]: {
maxWidth: '180px',
},
transition: 'max-width .15s',
userSelect: 'none',
...textEllipsis(1),
};
});
export const StyledTitleWrapper = styled('div')({
height: '100%',
position: 'relative',
...displayFlex('center', 'center'),
});
export const StyledHeaderRightSide = styled('div')({
height: '100%',
display: 'flex',
alignItems: 'center',
'>*:not(:last-child)': {
marginRight: '12px',
},
});
export const StyledBrowserWarning = styled('div')<{ show: boolean }>(
({ theme, show }) => {
return {
backgroundColor: theme.colors.warningBackground,
color: theme.colors.warningColor,
height: '36px',
fontSize: theme.font.sm,
display: show ? 'flex' : 'none',
justifyContent: 'center',
alignItems: 'center',
};
}
);
export const StyledCloseButton = styled('div')(({ theme }) => {
return {
width: '36px',
height: '36px',
color: theme.colors.iconColor,
cursor: 'pointer',
...displayFlex('center', 'center'),
position: 'absolute',
right: '15px',
top: '0',
svg: {
width: '15px',
height: '15px',
position: 'relative',
zIndex: 1,
},
};
});
export const StyledSwitchWrapper = styled('div')(() => {
return {
position: 'absolute',
right: '100%',
top: 0,
bottom: 0,
margin: 'auto',
...displayFlex('center', 'center'),
};
});
export const StyledSearchArrowWrapper = styled('div')(() => {
return {
position: 'absolute',
left: 'calc(100% + 4px)',
top: 0,
bottom: 0,
margin: 'auto',
...displayFlex('center', 'center'),
};
});
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
return {
fontSize: theme.font.base,
color: theme.colors.textColor,
...displayFlex('center', 'center'),
'>svg': {
fontSize: '20px',
marginRight: '12px',
},
};
});
export const StyledQuickSearchTipButton = styled('div')(({ theme }) => {
return {
...displayFlex('center', 'center'),
marginTop: '12px',
color: '#FFFFFF',
width: '48px',
height: ' 26px',
fontSize: theme.font.sm,
lineHeight: '22px',
background: theme.colors.primaryColor,
borderRadius: '8px',
textAlign: 'center',
cursor: 'pointer',
};
});
export const StyledQuickSearchTipContent = styled('div')(() => {
return {
...displayFlex('center', 'flex-end'),
flexDirection: 'column',
};
});
@@ -0,0 +1,50 @@
import { getEnvironment } from '@affine/env';
import { Trans, useTranslation } from '@affine/i18n';
import type React from 'react';
import { useEffect, useState } from 'react';
const minimumChromeVersion = 102;
export const shouldShowWarning = () => {
const env = getEnvironment();
if (env.isDesktop) {
// even though desktop have compatibility issues, we don't want to show the warning
return false;
}
if (!env.isBrowser) {
// disable in SSR
return false;
}
if (env.isChrome) {
return env.chromeVersion < minimumChromeVersion;
} else {
return !env.isMobile;
}
};
export const OSWarningMessage: React.FC = () => {
const { t } = useTranslation();
const [notChrome, setNotChrome] = useState(false);
const [notGoodVersion, setNotGoodVersion] = useState(false);
useEffect(() => {
const env = getEnvironment();
setNotChrome(env.isBrowser && !env.isChrome);
setNotGoodVersion(
env.isBrowser && env.isChrome && env.chromeVersion < minimumChromeVersion
);
}, []);
if (notChrome) {
return (
<span>
<Trans i18nKey="recommendBrowser">
We recommend the <strong>Chrome</strong> browser for optimal
experience.
</Trans>
</span>
);
} else if (notGoodVersion) {
return <span>{t('upgradeBrowser')}</span>;
}
return null;
};