mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 13:29:02 +08:00
Merge branch 'canary' into stable
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { StorybookConfig } from '@storybook/react-vite';
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { mergeConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { getRuntimeConfig } from '../../core/.webpack/runtime-config';
|
||||
|
||||
export default {
|
||||
stories: ['../src/ui/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-mdx-gfm',
|
||||
'storybook-dark-mode',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
features: {
|
||||
storyStoreV7: true,
|
||||
},
|
||||
docs: {
|
||||
autodocs: true,
|
||||
},
|
||||
async viteFinal(config, _options) {
|
||||
return mergeConfig(config, {
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
tsconfigPaths({
|
||||
root: fileURLToPath(new URL('../../../../', import.meta.url)),
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
'process.on': '(() => void 0)',
|
||||
'process.env': {},
|
||||
'process.env.COVERAGE': JSON.stringify(!!process.env.COVERAGE),
|
||||
'process.env.SHOULD_REPORT_TRACE': `${Boolean(
|
||||
process.env.SHOULD_REPORT_TRACE === 'true'
|
||||
)}`,
|
||||
'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`,
|
||||
'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`,
|
||||
runtimeConfig: getRuntimeConfig({
|
||||
distribution: 'browser',
|
||||
mode: 'development',
|
||||
channel: 'canary',
|
||||
coverage: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
} satisfies StorybookConfig;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { darkCssVariables, lightCssVariables } from '@toeverything/theme';
|
||||
import { globalStyle } from '@vanilla-extract/css';
|
||||
|
||||
globalStyle('*', {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
});
|
||||
|
||||
globalStyle('body', {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontFamily: 'var(--affine-font-family)',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: 'var(--affine-font-height)',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
});
|
||||
|
||||
globalStyle('html', {
|
||||
vars: lightCssVariables,
|
||||
});
|
||||
|
||||
globalStyle('html[data-theme="dark"]', {
|
||||
vars: darkCssVariables,
|
||||
});
|
||||
|
||||
globalStyle('.docs-story', {
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
import './preview.css';
|
||||
import { ThemeProvider, useTheme } from 'next-themes';
|
||||
import type { ComponentType } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useDarkMode } from 'storybook-dark-mode';
|
||||
|
||||
import type { Preview } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
export const parameters: Preview = {
|
||||
argTypes: {
|
||||
param: {
|
||||
table: { category: 'Group' },
|
||||
},
|
||||
},
|
||||
globalTypes: {
|
||||
theme: {
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'light',
|
||||
toolbar: {
|
||||
title: 'Theme',
|
||||
icon: 'circlehollow',
|
||||
items: ['light', 'dark'],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ThemeChange = () => {
|
||||
const isDark = useDarkMode();
|
||||
const theme = useTheme();
|
||||
if (theme.resolvedTheme === 'dark' && !isDark) {
|
||||
theme.setTheme('light');
|
||||
} else if (theme.resolvedTheme === 'light' && isDark) {
|
||||
theme.setTheme('dark');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const Component = () => {
|
||||
const isDark = useDarkMode();
|
||||
const theme = useTheme();
|
||||
useEffect(() => {
|
||||
theme.setTheme(isDark ? 'dark' : 'light');
|
||||
}, [isDark]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story: ComponentType, context) => {
|
||||
return (
|
||||
<ThemeProvider themes={['dark', 'light']} enableSystem={true}>
|
||||
<ThemeChange />
|
||||
<Component />
|
||||
<Story {...context} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
];
|
||||
@@ -5,13 +5,17 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./theme/*": "./src/theme/*",
|
||||
"./ui/*": "./src/ui/*/index.ts",
|
||||
"./*": "./src/components/*/index.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "storybook dev -p 6006"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blocksuite/blocks": "*",
|
||||
"@blocksuite/editor": "*",
|
||||
"@blocksuite/global": "*",
|
||||
"@blocksuite/icons": "2.1.34",
|
||||
"@blocksuite/presets": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,7 +24,7 @@
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
@@ -29,11 +33,14 @@
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-toolbar": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@toeverything/theme": "^0.7.24",
|
||||
@@ -64,13 +71,24 @@
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/blocks": "0.11.0-nightly-202312070955-2b5bb47",
|
||||
"@blocksuite/global": "0.11.0-nightly-202312070955-2b5bb47",
|
||||
"@blocksuite/icons": "2.1.36",
|
||||
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
|
||||
"@blocksuite/lit": "0.11.0-nightly-202312070955-2b5bb47",
|
||||
"@blocksuite/presets": "0.11.0-nightly-202312070955-2b5bb47",
|
||||
"@blocksuite/store": "0.11.0-nightly-202312070955-2b5bb47",
|
||||
"@storybook/addon-actions": "^7.5.3",
|
||||
"@storybook/addon-essentials": "^7.5.3",
|
||||
"@storybook/addon-interactions": "^7.5.3",
|
||||
"@storybook/addon-links": "^7.5.3",
|
||||
"@storybook/addon-mdx-gfm": "^7.5.3",
|
||||
"@storybook/addon-storysource": "^7.5.3",
|
||||
"@storybook/blocks": "^7.5.3",
|
||||
"@storybook/builder-vite": "^7.5.3",
|
||||
"@storybook/jest": "^0.2.3",
|
||||
"@storybook/react": "^7.5.3",
|
||||
"@storybook/react-vite": "^7.5.3",
|
||||
"@storybook/test-runner": "^0.15.2",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/bytes": "^3.1.3",
|
||||
@@ -80,8 +98,10 @@
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"storybook": "^7.5.3",
|
||||
"storybook-dark-mode": "^3.0.1",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^4.4.11",
|
||||
"vite": "^5.0.6",
|
||||
"vitest": "0.34.6",
|
||||
"yjs": "^13.6.10"
|
||||
},
|
||||
|
||||
@@ -40,6 +40,11 @@ export const tipsContainer = style({
|
||||
position: 'sticky',
|
||||
gap: '16px',
|
||||
containerType: 'inline-size',
|
||||
'@media': {
|
||||
'screen and (max-width: 520px)': {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const tipsMessage = style({
|
||||
@@ -54,4 +59,9 @@ export const tipsRightItem = style({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
'@media': {
|
||||
'screen and (max-width: 520px)': {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, IconButton } from '@affine/component/ui/button';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
@@ -17,13 +18,10 @@ export const LocalDemoTips = ({
|
||||
onLogin,
|
||||
onEnableCloud,
|
||||
}: LocalDemoTipsProps) => {
|
||||
const content = isLoggedIn
|
||||
? 'This is a local demo workspace, and the data is stored locally. We recommend enabling AFFiNE Cloud.'
|
||||
: 'This is a local demo workspace, and the data is stored locally in the browser. We recommend Enabling AFFiNE Cloud or downloading the client for a better experience.';
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
const buttonLabel = isLoggedIn
|
||||
? 'Enable AFFiNE Cloud'
|
||||
: 'Sign in with AFFiNE Cloud';
|
||||
? t['Enable AFFiNE Cloud']()
|
||||
: t['Sign in and Enable']();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (isLoggedIn) {
|
||||
@@ -34,7 +32,9 @@ export const LocalDemoTips = ({
|
||||
|
||||
return (
|
||||
<div className={styles.tipsContainer} data-testid="local-demo-tips">
|
||||
<div className={styles.tipsMessage}>{content}</div>
|
||||
<div className={styles.tipsMessage}>
|
||||
{t['com.affine.banner.local-warning']()}
|
||||
</div>
|
||||
|
||||
<div className={styles.tipsRightItem}>
|
||||
<div>
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import * as styles from './index.css';
|
||||
import { useNavConfig } from './use-nav-config';
|
||||
|
||||
export const DesktopNavbar = () => {
|
||||
const config = useNavConfig();
|
||||
|
||||
return (
|
||||
<div className={styles.topNavLinks}>
|
||||
{config.map(item => {
|
||||
return (
|
||||
<a
|
||||
key={item.title}
|
||||
href={item.path}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.topNavLink}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const root = style({
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
position: 'relative',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
});
|
||||
|
||||
export const affineLogo = style({
|
||||
color: 'inherit',
|
||||
});
|
||||
|
||||
export const topNav = style({
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 120px',
|
||||
selectors: {
|
||||
'&.mobile': {
|
||||
padding: '16px 20px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const topNavLinks = style({
|
||||
display: 'flex',
|
||||
columnGap: 4,
|
||||
});
|
||||
|
||||
export const topNavLink = style({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
padding: '4px 18px',
|
||||
});
|
||||
|
||||
export const iconButton = style({
|
||||
fontSize: '24px',
|
||||
pointerEvents: 'auto',
|
||||
selectors: {
|
||||
'&.plain': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const menu = style({
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
padding: '0',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
borderRadius: '0',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
});
|
||||
|
||||
export const menuItem = style({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
padding: '12px 20px',
|
||||
maxWidth: '100%',
|
||||
position: 'relative',
|
||||
borderRadius: '0',
|
||||
transition: 'background 0.3s ease',
|
||||
selectors: {
|
||||
'&:after': {
|
||||
position: 'absolute',
|
||||
content: '""',
|
||||
bottom: 0,
|
||||
display: 'block',
|
||||
width: 'calc(100% - 40px)',
|
||||
height: '0.5px',
|
||||
background: 'var(--affine-black-10)',
|
||||
},
|
||||
'&:not(:last-of-type)': {
|
||||
marginBottom: '0',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './layout';
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DesktopNavbar } from './desktop-navbar';
|
||||
import * as styles from './index.css';
|
||||
import { MobileNavbar } from './mobile-navbar';
|
||||
|
||||
export const AffineOtherPageLayout = ({
|
||||
isSmallScreen,
|
||||
children,
|
||||
}: {
|
||||
isSmallScreen: boolean;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const openDownloadLink = useCallback(() => {
|
||||
open(runtimeConfig.downloadUrl, '_blank');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div
|
||||
className={clsx(styles.topNav, {
|
||||
mobile: isSmallScreen,
|
||||
})}
|
||||
>
|
||||
<a href="/" rel="noreferrer" className={styles.affineLogo}>
|
||||
<Logo1Icon width={24} height={24} />
|
||||
</a>
|
||||
{isSmallScreen ? (
|
||||
<MobileNavbar />
|
||||
) : (
|
||||
<>
|
||||
<DesktopNavbar />
|
||||
<Button onClick={openDownloadLink}>
|
||||
{t['com.affine.auth.open.affine.download-app']()}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { IconButton } from '@affine/component/ui/button';
|
||||
import { Menu, MenuItem } from '@affine/component/ui/menu';
|
||||
import { CloseIcon, PropertyIcon } from '@blocksuite/icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
import { useNavConfig } from './use-nav-config';
|
||||
|
||||
export const MobileNavbar = () => {
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
const navConfig = useNavConfig();
|
||||
|
||||
const menuItems = (
|
||||
<>
|
||||
{navConfig.map(item => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={item.title}
|
||||
onClick={() => {
|
||||
open(item.path, '_blank');
|
||||
}}
|
||||
className={styles.menuItem}
|
||||
>
|
||||
{item.title}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu
|
||||
items={menuItems}
|
||||
contentOptions={{
|
||||
className: styles.menu,
|
||||
sideOffset: 20,
|
||||
}}
|
||||
rootOptions={{
|
||||
open: openMenu,
|
||||
onOpenChange: setOpenMenu,
|
||||
}}
|
||||
>
|
||||
<IconButton type="plain" className={styles.iconButton}>
|
||||
{openMenu ? <CloseIcon /> : <PropertyIcon />}
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNavConfig = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t['com.affine.other-page.nav.official-website'](),
|
||||
path: 'https://affine.pro',
|
||||
},
|
||||
{
|
||||
title: t['com.affine.other-page.nav.affine-community'](),
|
||||
path: 'https://community.affine.pro/home',
|
||||
},
|
||||
{
|
||||
title: t['com.affine.other-page.nav.blog'](),
|
||||
path: 'https://affine.pro/blog',
|
||||
},
|
||||
{
|
||||
title: t['com.affine.other-page.nav.contact-us'](),
|
||||
path: 'https://affine.pro/about-us',
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
};
|
||||
+2
-1
@@ -85,12 +85,12 @@ export const installLabel = style({
|
||||
flex: 1,
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
whiteSpace: 'nowrap',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
export const installLabelNormal = style([
|
||||
installLabel,
|
||||
{
|
||||
justifyContent: 'space-between',
|
||||
selectors: {
|
||||
[`${root}:hover &, ${root}[data-updating=true] &`]: {
|
||||
display: 'none',
|
||||
@@ -103,6 +103,7 @@ export const installLabelHover = style([
|
||||
installLabel,
|
||||
{
|
||||
display: 'none',
|
||||
justifyContent: 'flex-start',
|
||||
selectors: {
|
||||
[`${root}:hover &, ${root}[data-updating=true] &`]: {
|
||||
display: 'flex',
|
||||
|
||||
+207
-136
@@ -1,20 +1,11 @@
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import {
|
||||
changelogCheckedAtom,
|
||||
currentChangelogUnreadAtom,
|
||||
currentVersionAtom,
|
||||
downloadProgressAtom,
|
||||
updateAvailableAtom,
|
||||
updateReadyAtom,
|
||||
useAppUpdater,
|
||||
} from '@toeverything/hooks/use-app-updater';
|
||||
import { useAppUpdater } from '@toeverything/hooks/use-app-updater';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { startTransition, useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { Tooltip } from '../../../ui/tooltip';
|
||||
import * as styles from './index.css';
|
||||
|
||||
export interface AddPageButtonPureProps {
|
||||
@@ -26,36 +17,178 @@ export interface AddPageButtonPureProps {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
} | null;
|
||||
autoDownload: boolean;
|
||||
downloadProgress: number | null;
|
||||
appQuitting: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface ButtonContentProps {
|
||||
updateReady: boolean;
|
||||
updateAvailable: {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
} | null;
|
||||
autoDownload: boolean;
|
||||
downloadProgress: number | null;
|
||||
appQuitting: boolean;
|
||||
currentChangelogUnread: boolean;
|
||||
onDismissCurrentChangelog: () => void;
|
||||
}
|
||||
|
||||
function DownloadUpdate({ updateAvailable }: ButtonContentProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div className={styles.installLabel}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.downloadUpdate']()}
|
||||
</span>
|
||||
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateReady({ updateAvailable, appQuitting }: ButtonContentProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div className={styles.updateAvailableWrapper}>
|
||||
<div className={styles.installLabelNormal}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.updateAvailable']()}
|
||||
</span>
|
||||
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.installLabelHover}>
|
||||
<ResetIcon className={styles.icon} />
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t[appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate']()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadingUpdate({
|
||||
updateAvailable,
|
||||
downloadProgress,
|
||||
}: ButtonContentProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div className={clsx([styles.updateAvailableWrapper])}>
|
||||
<div className={clsx([styles.installLabelNormal])}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.downloading']()}
|
||||
</span>
|
||||
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.progress}>
|
||||
<div
|
||||
className={styles.progressInner}
|
||||
style={{ width: `${downloadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenDownloadPage({ updateAvailable }: ButtonContentProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<>
|
||||
<div className={styles.installLabelNormal}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.updateAvailable']()}
|
||||
</span>
|
||||
<span className={styles.versionLabel}>{updateAvailable?.version}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.installLabelHover}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.openDownloadPage']()}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function WhatsNew({ onDismissCurrentChangelog }: ButtonContentProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
const onClickClose: React.MouseEventHandler = useCallback(
|
||||
e => {
|
||||
onDismissCurrentChangelog();
|
||||
e.stopPropagation();
|
||||
},
|
||||
[onDismissCurrentChangelog]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className={clsx([styles.whatsNewLabel])}>
|
||||
<NewIcon className={styles.icon} />
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.whatsNew']()}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.closeIcon} onClick={onClickClose}>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getButtonContentRenderer = (props: ButtonContentProps) => {
|
||||
if (props.updateReady) {
|
||||
return UpdateReady;
|
||||
} else if (props.updateAvailable?.allowAutoUpdate) {
|
||||
if (props.autoDownload && props.updateAvailable.allowAutoUpdate) {
|
||||
return DownloadingUpdate;
|
||||
} else {
|
||||
return DownloadUpdate;
|
||||
}
|
||||
} else if (props.updateAvailable && !props.updateAvailable?.allowAutoUpdate) {
|
||||
return OpenDownloadPage;
|
||||
} else if (props.currentChangelogUnread) {
|
||||
return WhatsNew;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export function AppUpdaterButtonPure({
|
||||
updateReady,
|
||||
onClickUpdate,
|
||||
onDismissCurrentChangelog,
|
||||
currentChangelogUnread,
|
||||
updateAvailable,
|
||||
autoDownload,
|
||||
downloadProgress,
|
||||
appQuitting,
|
||||
className,
|
||||
style,
|
||||
}: AddPageButtonPureProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
const contentProps = useMemo(
|
||||
() => ({
|
||||
updateReady,
|
||||
updateAvailable,
|
||||
currentChangelogUnread,
|
||||
autoDownload,
|
||||
downloadProgress,
|
||||
appQuitting,
|
||||
onDismissCurrentChangelog,
|
||||
}),
|
||||
[
|
||||
updateReady,
|
||||
updateAvailable,
|
||||
currentChangelogUnread,
|
||||
autoDownload,
|
||||
downloadProgress,
|
||||
appQuitting,
|
||||
onDismissCurrentChangelog,
|
||||
]
|
||||
);
|
||||
|
||||
if (!updateAvailable && !currentChangelogUnread) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateAvailableNode = updateAvailable
|
||||
? updateAvailable.allowAutoUpdate
|
||||
? renderUpdateAvailableAllowAutoUpdate()
|
||||
: renderUpdateAvailableNotAllowAutoUpdate()
|
||||
: null;
|
||||
const whatsNew =
|
||||
!updateAvailable && currentChangelogUnread ? renderWhatsNew() : null;
|
||||
const ContentComponent = getButtonContentRenderer(contentProps);
|
||||
|
||||
const wrapWithTooltip = (
|
||||
node: React.ReactElement,
|
||||
@@ -72,102 +205,38 @@ export function AppUpdaterButtonPure({
|
||||
);
|
||||
};
|
||||
|
||||
const disabled = useMemo(() => {
|
||||
if (appQuitting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (updateAvailable?.allowAutoUpdate) {
|
||||
return !updateReady && autoDownload;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [
|
||||
appQuitting,
|
||||
autoDownload,
|
||||
updateAvailable?.allowAutoUpdate,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
return wrapWithTooltip(
|
||||
<button
|
||||
style={style}
|
||||
className={clsx([styles.root, className])}
|
||||
data-has-update={!!updateAvailable}
|
||||
data-updating={appQuitting}
|
||||
data-disabled={
|
||||
(updateAvailable?.allowAutoUpdate && !updateReady) || appQuitting
|
||||
}
|
||||
data-disabled={disabled}
|
||||
onClick={onClickUpdate}
|
||||
>
|
||||
{updateAvailableNode}
|
||||
{whatsNew}
|
||||
{ContentComponent ? <ContentComponent {...contentProps} /> : null}
|
||||
<div className={styles.particles} aria-hidden="true"></div>
|
||||
<span className={styles.halo} aria-hidden="true"></span>
|
||||
</button>,
|
||||
updateAvailable?.version
|
||||
);
|
||||
|
||||
function renderUpdateAvailableAllowAutoUpdate() {
|
||||
return (
|
||||
<div className={clsx([styles.updateAvailableWrapper])}>
|
||||
<div className={clsx([styles.installLabelNormal])}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{!updateReady
|
||||
? t['com.affine.appUpdater.downloading']()
|
||||
: t['com.affine.appUpdater.updateAvailable']()}
|
||||
</span>
|
||||
<span className={styles.versionLabel}>
|
||||
{updateAvailable?.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{updateReady ? (
|
||||
<div className={clsx([styles.installLabelHover])}>
|
||||
<ResetIcon className={styles.icon} />
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t[
|
||||
appQuitting ? 'Loading' : 'com.affine.appUpdater.installUpdate'
|
||||
]()}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.progress}>
|
||||
<div
|
||||
className={styles.progressInner}
|
||||
style={{ width: `${downloadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderUpdateAvailableNotAllowAutoUpdate() {
|
||||
return (
|
||||
<>
|
||||
<div className={clsx([styles.installLabelNormal])}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.updateAvailable']()}
|
||||
</span>
|
||||
<span className={styles.versionLabel}>
|
||||
{updateAvailable?.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={clsx([styles.installLabelHover])}>
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.openDownloadPage']()}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderWhatsNew() {
|
||||
return (
|
||||
<>
|
||||
<div className={clsx([styles.whatsNewLabel])}>
|
||||
<NewIcon className={styles.icon} />
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.appUpdater.whatsNew']()}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.closeIcon}
|
||||
onClick={e => {
|
||||
onDismissCurrentChangelog();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Although it is called an input, it is actually a button.
|
||||
@@ -178,62 +247,64 @@ export function AppUpdaterButton({
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom);
|
||||
const updateReady = useAtomValue(updateReadyAtom);
|
||||
const updateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const downloadProgress = useAtomValue(downloadProgressAtom);
|
||||
const currentVersion = useAtomValue(currentVersionAtom);
|
||||
const { quitAndInstall, appQuitting } = useAppUpdater();
|
||||
const setChangelogCheckAtom = useSetAtom(changelogCheckedAtom);
|
||||
|
||||
const dismissCurrentChangelog = useCallback(() => {
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
startTransition(() =>
|
||||
setChangelogCheckAtom(mapping => {
|
||||
return {
|
||||
...mapping,
|
||||
[currentVersion]: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [currentVersion, setChangelogCheckAtom]);
|
||||
const {
|
||||
quitAndInstall,
|
||||
appQuitting,
|
||||
autoDownload,
|
||||
downloadUpdate,
|
||||
readChangelog,
|
||||
changelogUnread,
|
||||
updateReady,
|
||||
updateAvailable,
|
||||
downloadProgress,
|
||||
currentVersion,
|
||||
} = useAppUpdater();
|
||||
|
||||
const handleClickUpdate = useCallback(() => {
|
||||
if (updateReady) {
|
||||
quitAndInstall();
|
||||
} else if (updateAvailable) {
|
||||
if (updateAvailable.allowAutoUpdate) {
|
||||
// wait for download to finish
|
||||
if (autoDownload) {
|
||||
// wait for download to finish
|
||||
} else {
|
||||
downloadUpdate();
|
||||
}
|
||||
} else {
|
||||
window.open(
|
||||
`https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
} else if (currentChangelogUnread) {
|
||||
} else if (changelogUnread) {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
dismissCurrentChangelog();
|
||||
readChangelog();
|
||||
} else {
|
||||
throw new Unreachable();
|
||||
}
|
||||
}, [
|
||||
updateReady,
|
||||
quitAndInstall,
|
||||
updateAvailable,
|
||||
currentChangelogUnread,
|
||||
dismissCurrentChangelog,
|
||||
changelogUnread,
|
||||
quitAndInstall,
|
||||
autoDownload,
|
||||
downloadUpdate,
|
||||
currentVersion,
|
||||
readChangelog,
|
||||
]);
|
||||
|
||||
if (!updateAvailable && !changelogUnread) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppUpdaterButtonPure
|
||||
appQuitting={appQuitting}
|
||||
autoDownload={autoDownload}
|
||||
updateReady={!!updateReady}
|
||||
onClickUpdate={handleClickUpdate}
|
||||
onDismissCurrentChangelog={dismissCurrentChangelog}
|
||||
currentChangelogUnread={currentChangelogUnread}
|
||||
onDismissCurrentChangelog={readChangelog}
|
||||
currentChangelogUnread={changelogUnread}
|
||||
updateAvailable={updateAvailable}
|
||||
downloadProgress={downloadProgress}
|
||||
className={className}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { IconButton } from '../../../ui/button';
|
||||
import { Tooltip } from '../../../ui/tooltip';
|
||||
import type { History } from '..';
|
||||
import {
|
||||
navHeaderButton,
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SidebarIcon } from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { IconButton } from '../../../ui/button';
|
||||
import { Tooltip } from '../../../ui/tooltip';
|
||||
import { appSidebarOpenAtom } from '../index.jotai';
|
||||
import * as styles from './sidebar-switch.css';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||
import type { FC, HTMLAttributes } from 'react';
|
||||
|
||||
import { Input, type InputProps } from '../../ui/input';
|
||||
import { authInputWrapper, formHint } from './share.css';
|
||||
import * as styles from './share.css';
|
||||
export type AuthInputProps = InputProps & {
|
||||
label?: string;
|
||||
error?: boolean;
|
||||
@@ -22,13 +22,14 @@ export const AuthInput: FC<AuthInputProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(authInputWrapper, className, {
|
||||
className={clsx(styles.authInputWrapper, className, {
|
||||
'without-hint': withoutHint,
|
||||
})}
|
||||
{...otherWrapperProps}
|
||||
>
|
||||
{label ? <label>{label}</label> : null}
|
||||
<Input
|
||||
className={styles.input}
|
||||
size="extraLarge"
|
||||
status={error ? 'error' : 'default'}
|
||||
onKeyDown={e => {
|
||||
@@ -40,7 +41,7 @@ export const AuthInput: FC<AuthInputProps> = ({
|
||||
/>
|
||||
{error && errorHint && !withoutHint ? (
|
||||
<div
|
||||
className={clsx(formHint, {
|
||||
className={clsx(styles.formHint, {
|
||||
error: error,
|
||||
})}
|
||||
>
|
||||
|
||||
+29
-20
@@ -1,32 +1,41 @@
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { Empty } from '../../ui/empty';
|
||||
import { Wrapper } from '../../ui/layout';
|
||||
import { Logo } from './logo';
|
||||
import { AffineOtherPageLayout } from '../affine-other-page-layout';
|
||||
import { authPageContainer } from './share.css';
|
||||
|
||||
export const AuthPageContainer: FC<
|
||||
PropsWithChildren<{ title?: ReactNode; subtitle?: ReactNode }>
|
||||
> = ({ children, title, subtitle }) => {
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkScreenSize = () => {
|
||||
setIsSmallScreen(window.innerWidth <= 1024);
|
||||
};
|
||||
checkScreenSize();
|
||||
window.addEventListener('resize', checkScreenSize);
|
||||
return () => window.removeEventListener('resize', checkScreenSize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={authPageContainer}>
|
||||
<Wrapper
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 25,
|
||||
left: 20,
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
</Wrapper>
|
||||
<div className="wrapper">
|
||||
<div className="content">
|
||||
<p className="title">{title}</p>
|
||||
<p className="subtitle">{subtitle}</p>
|
||||
{children}
|
||||
<AffineOtherPageLayout isSmallScreen={isSmallScreen}>
|
||||
<div className={authPageContainer}>
|
||||
<div className="wrapper">
|
||||
<div className="content">
|
||||
<p className="title">{title}</p>
|
||||
<p className="subtitle">{subtitle}</p>
|
||||
{children}
|
||||
</div>
|
||||
{isSmallScreen ? null : <Empty />}
|
||||
</div>
|
||||
<Empty />
|
||||
</div>
|
||||
</div>
|
||||
</AffineOtherPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons';
|
||||
import { Button, type ButtonProps } from '@toeverything/components/button';
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { Button, type ButtonProps } from '../../ui/button';
|
||||
|
||||
export const BackButton: FC<ButtonProps> = props => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { AuthInput } from './auth-input';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
import { emailRegex } from './utils';
|
||||
@@ -44,7 +44,6 @@ export const ChangeEmailPage = ({
|
||||
>
|
||||
<>
|
||||
<AuthInput
|
||||
width={320}
|
||||
label={t['com.affine.settings.email']()}
|
||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||
value={email}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { pushNotificationAtom } from '../notification-center';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
import { SetPassword } from './set-password';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
|
||||
export const ConfirmChangeEmail: FC<{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import { Modal } from '../../ui/modal';
|
||||
|
||||
export type AuthModalProps = {
|
||||
open: boolean;
|
||||
setOpen: (value: boolean) => void;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type FC, useEffect } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Input, type InputProps } from '../../../ui/input';
|
||||
import * as styles from '../share.css';
|
||||
import { ErrorIcon } from './error';
|
||||
import { SuccessIcon } from './success';
|
||||
import { Tag } from './tag';
|
||||
@@ -74,6 +75,7 @@ export const PasswordInput: FC<
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className={styles.input}
|
||||
type="password"
|
||||
size="extraLarge"
|
||||
style={{ marginBottom: 20 }}
|
||||
@@ -83,6 +85,7 @@ export const PasswordInput: FC<
|
||||
{...inputProps}
|
||||
/>
|
||||
<Input
|
||||
className={styles.input}
|
||||
type="password"
|
||||
size="extraLarge"
|
||||
placeholder={t['com.affine.auth.set.password.placeholder.confirm']()}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { pushNotificationAtom } from '../notification-center';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
import { SetPassword } from './set-password';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { type FC, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { Wrapper } from '../../ui/layout';
|
||||
import { PasswordInput } from './password-input';
|
||||
|
||||
@@ -19,7 +19,6 @@ export const SetPassword: FC<{
|
||||
<>
|
||||
<Wrapper marginTop={30} marginBottom={42}>
|
||||
<PasswordInput
|
||||
width={320}
|
||||
onPass={useCallback(password => {
|
||||
setPasswordPass(true);
|
||||
passwordRef.current = password;
|
||||
|
||||
@@ -151,14 +151,25 @@ export const authPageContainer = style({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
'@media': {
|
||||
'screen and (max-width: 1024px)': {
|
||||
flexDirection: 'column',
|
||||
padding: '100px 20px',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${authPageContainer} .wrapper`, {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'@media': {
|
||||
'screen and (max-width: 1024px)': {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${authPageContainer} .content`, {
|
||||
maxWidth: '700px',
|
||||
minWidth: '550px',
|
||||
});
|
||||
|
||||
globalStyle(`${authPageContainer} .title`, {
|
||||
@@ -178,3 +189,12 @@ export const signInPageContainer = style({
|
||||
width: '400px',
|
||||
margin: '205px auto 0',
|
||||
});
|
||||
|
||||
export const input = style({
|
||||
width: '330px',
|
||||
'@media': {
|
||||
'screen and (max-width: 520px)': {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
|
||||
export const SignInSuccessPage: FC<{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { pushNotificationAtom } from '../notification-center';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
import { SetPassword } from './set-password';
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const RootBlockHub = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const blockHub = useAtomValue(rootBlockHubAtom);
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const div = ref.current;
|
||||
if (blockHub) {
|
||||
if (div.hasChildNodes()) {
|
||||
(div.firstChild as ChildNode).remove();
|
||||
}
|
||||
div.append(blockHub);
|
||||
}
|
||||
}
|
||||
}, [blockHub]);
|
||||
return <div ref={ref} data-testid="block-hub" />;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EditorContainer } from '@blocksuite/presets';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import clsx from 'clsx';
|
||||
import { use } from 'foxact/use';
|
||||
|
||||
@@ -2,16 +2,16 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Avatar } from '../../../ui/avatar';
|
||||
import { Divider } from '../../../ui/divider';
|
||||
import { Skeleton } from '../../../ui/skeleton';
|
||||
import { Tooltip } from '../../../ui/tooltip';
|
||||
import {
|
||||
StyledCard,
|
||||
StyledIconContainer,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
|
||||
import { displayFlex, styled, textEllipsis } from '../../../styles';
|
||||
import { IconButton } from '../../../ui/button';
|
||||
|
||||
export const StyledWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
ConfirmModal,
|
||||
type ConfirmModalProps,
|
||||
} from '@toeverything/components/modal';
|
||||
|
||||
import { ConfirmModal, type ConfirmModalProps } from '../../ui/modal';
|
||||
|
||||
export const PublicLinkDisableModal = (props: ConfirmModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
NewIcon,
|
||||
NotionIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
|
||||
import { IconButton } from '../../ui/button';
|
||||
import { Tooltip } from '../../ui/tooltip';
|
||||
import { BlockCard } from '../card/block-card';
|
||||
import {
|
||||
importPageBodyStyle,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AuthPageContainer } from '@affine/component/auth-components';
|
||||
import { type GetInviteInfoQuery } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
import { Avatar } from '../../ui/avatar';
|
||||
import { Button } from '../../ui/button';
|
||||
import { FlexWrapper } from '../../ui/layout';
|
||||
import * as styles from './styles.css';
|
||||
export const AcceptInvitePage = ({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Permission } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ConfirmModal } from '@toeverything/components/modal';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { ConfirmModal } from '../../ui/modal';
|
||||
import { AuthInput } from '..//auth-components';
|
||||
import { emailRegex } from '..//auth-components/utils';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SignOutIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
|
||||
import { Avatar } from '../../ui/avatar';
|
||||
import { Button, IconButton } from '../../ui/button';
|
||||
import { Tooltip } from '../../ui/tooltip';
|
||||
import { NotFoundPattern } from './not-found-pattern';
|
||||
import {
|
||||
largeButtonEffect,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const notFoundPageContainer = style({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100vw',
|
||||
padding: '0 20px',
|
||||
});
|
||||
|
||||
export const wrapper = style({
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons';
|
||||
import * as Toast from '@radix-ui/react-toast';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { IconButton } from '../../ui/button';
|
||||
import { SuccessIcon } from './icons';
|
||||
import * as styles from './index.css';
|
||||
import type { Notification } from './index.jotai';
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
IconButton,
|
||||
type IconButtonProps,
|
||||
} from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import Lottie from 'lottie-react';
|
||||
import { forwardRef, useCallback, useState } from 'react';
|
||||
|
||||
import { IconButton, type IconButtonProps } from '../../../ui/button';
|
||||
import { Tooltip } from '../../../ui/tooltip';
|
||||
import favoritedAnimation from './favorited-animation/data.json';
|
||||
|
||||
export const FavoriteTag = forwardRef<
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, ImportIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { type PropsWithChildren, useCallback, useState } from 'react';
|
||||
|
||||
import { DropdownButton } from '../../../ui/button';
|
||||
import { Menu } from '../../../ui/menu';
|
||||
import { BlockCard } from '../../card/block-card';
|
||||
import { menuContent } from './new-page-button.css';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Filter, Literal } from '@affine/env/filter';
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import { Menu, MenuItem } from '@toeverything/components/menu';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Menu, MenuItem } from '../../../ui/menu';
|
||||
import { FilterTag } from './filter-tag-translation';
|
||||
import * as styles from './index.css';
|
||||
import { literalMatcher } from './literal-matcher';
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { Filter } from '@affine/env/filter';
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, PlusIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
|
||||
import { Button } from '../../../ui/button';
|
||||
import { IconButton } from '../../../ui/button';
|
||||
import { Menu } from '../../../ui/menu';
|
||||
import { Condition } from './condition';
|
||||
import * as styles from './index.css';
|
||||
import { CreateFilterMenu } from './vars';
|
||||
|
||||
@@ -19,6 +19,8 @@ const useFilterTag = ({ name }: FilterTagProps) => {
|
||||
return t['com.affine.filter.after']();
|
||||
case 'before':
|
||||
return t['com.affine.filter.before']();
|
||||
case 'last':
|
||||
return t['com.affine.filter.last']();
|
||||
case 'is':
|
||||
return t['com.affine.filter.is']();
|
||||
case 'is not empty':
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { LiteralValue, Tag } from '@affine/env/filter';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import Input from '../../../ui/input';
|
||||
import { Menu, MenuItem } from '../../../ui/menu';
|
||||
import { AFFiNEDatePicker } from '../../date-picker';
|
||||
import { FilterTag } from './filter-tag-translation';
|
||||
import { inputStyle } from './index.css';
|
||||
import { tBoolean, tDate, tTag } from './logical/custom-type';
|
||||
import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type';
|
||||
import { Matcher } from './logical/matcher';
|
||||
import type { TType } from './logical/typesystem';
|
||||
import { tArray, typesystem } from './logical/typesystem';
|
||||
@@ -21,6 +23,37 @@ export const literalMatcher = new Matcher<{
|
||||
return typesystem.isSubtype(type, target);
|
||||
});
|
||||
|
||||
literalMatcher.register(tDateRange.create(), {
|
||||
render: ({ value, onChange }) => (
|
||||
<Menu
|
||||
items={
|
||||
<div>
|
||||
<Input
|
||||
type="number"
|
||||
// Handle the input change and update the value accordingly
|
||||
onChange={i => (i ? onChange(parseInt(i)) : onChange(0))}
|
||||
/>
|
||||
{[1, 2, 3, 7, 14, 30].map(i => (
|
||||
<MenuItem
|
||||
key={i}
|
||||
onClick={() => {
|
||||
// Handle the menu item click and update the value accordingly
|
||||
onChange(i);
|
||||
}}
|
||||
>
|
||||
{i} {i > 1 ? 'days' : 'day'}
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<span>{value.toString()}</span> {(value as number) > 1 ? 'days' : 'day'}
|
||||
</div>
|
||||
</Menu>
|
||||
),
|
||||
});
|
||||
|
||||
literalMatcher.register(tBoolean.create(), {
|
||||
render: ({ value, onChange }) => (
|
||||
<div
|
||||
|
||||
@@ -19,3 +19,7 @@ export const tTag = typesystem.defineData<{ tags: Tag[] }>({
|
||||
name: 'Tag',
|
||||
supers: [],
|
||||
});
|
||||
|
||||
export const tDateRange = typesystem.defineData(
|
||||
DataHelper.create<{ value: number }>('DateRange')
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Menu, MenuItem } from '@toeverything/components/menu';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Menu, MenuItem } from '../../../ui/menu';
|
||||
import * as styles from './multi-select.css';
|
||||
|
||||
export const MultiSelect = ({
|
||||
|
||||
@@ -5,17 +5,13 @@ import type {
|
||||
VariableMap,
|
||||
} from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
} from '@toeverything/components/menu';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { MenuIcon, MenuItem, MenuSeparator } from '../../../ui/menu';
|
||||
import { FilterTag } from './filter-tag-translation';
|
||||
import * as styles from './index.css';
|
||||
import { tBoolean, tDate, tTag } from './logical/custom-type';
|
||||
import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type';
|
||||
import { Matcher } from './logical/matcher';
|
||||
import type { TFunction } from './logical/typesystem';
|
||||
import {
|
||||
@@ -165,6 +161,24 @@ filterMatcher.register(
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tDate.create(), tDateRange.create()],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'last',
|
||||
defaultArgs: () => [30], // Default to the last 30 days
|
||||
impl: (date, n) => {
|
||||
if (typeof date !== 'number' || typeof n !== 'number') {
|
||||
throw new Error('Argument type error: date and n must be numbers');
|
||||
}
|
||||
const startDate = dayjs().subtract(n, 'day').startOf('day').valueOf();
|
||||
return date > startDate;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tDate.create(), tDate.create()],
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
OpenInNewIcon,
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
|
||||
import { ConfirmModal } from '@toeverything/components/modal';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { IconButton } from '../../ui/button';
|
||||
import { Menu, MenuIcon, MenuItem } from '../../ui/menu';
|
||||
import { ConfirmModal } from '../../ui/modal';
|
||||
import { Tooltip } from '../../ui/tooltip';
|
||||
import { FavoriteTag } from './components/favorite-tag';
|
||||
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
|
||||
import * as styles from './page-list.css';
|
||||
@@ -66,6 +66,7 @@ export const OperationCell = ({
|
||||
</MenuItem>
|
||||
{!environment.isDesktop && (
|
||||
<Link
|
||||
className={styles.clearLinkStyle}
|
||||
onClick={stopPropagationWithoutPrevent}
|
||||
to={link}
|
||||
target={'_blank'}
|
||||
|
||||
+1
-5
@@ -1,11 +1,7 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ShareIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
type MenuItemProps,
|
||||
} from '@toeverything/components/menu';
|
||||
|
||||
import { MenuIcon, MenuItem, type MenuItemProps } from '../../../ui/menu';
|
||||
import { PublicLinkDisableModal } from '../../disable-public-link';
|
||||
|
||||
export const DisablePublicSharing = (props: MenuItemProps) => {
|
||||
|
||||
+1
-1
@@ -6,9 +6,9 @@ import {
|
||||
ExportToPdfIcon,
|
||||
ExportToPngIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { MenuIcon, MenuItem, MenuSub } from '@toeverything/components/menu';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
|
||||
import { MenuIcon, MenuItem, MenuSub } from '../../../ui/menu';
|
||||
import { transitionStyle } from './index.css';
|
||||
|
||||
interface ExportMenuItemProps<T> {
|
||||
|
||||
+3
-9
@@ -1,14 +1,8 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DeleteIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
type MenuItemProps,
|
||||
} from '@toeverything/components/menu';
|
||||
import {
|
||||
ConfirmModal,
|
||||
type ConfirmModalProps,
|
||||
} from '@toeverything/components/modal';
|
||||
|
||||
import { MenuIcon, MenuItem, type MenuItemProps } from '../../../ui/menu';
|
||||
import { ConfirmModal, type ConfirmModalProps } from '../../../ui/modal';
|
||||
|
||||
export const MoveToTrash = (props: MenuItemProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -120,3 +120,14 @@ export const favoriteCell = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const clearLinkStyle = style({
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
':visited': {
|
||||
color: 'inherit',
|
||||
},
|
||||
':active': {
|
||||
color: 'inherit',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Tag } from '@affine/env/filter';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Menu } from '../../ui/menu';
|
||||
import * as styles from './page-tags.css';
|
||||
import { stopPropagation } from './utils';
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ViewLayersIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '../../../ui/button';
|
||||
import { Tooltip } from '../../../ui/tooltip';
|
||||
import {
|
||||
type CollectionsCRUDAtom,
|
||||
useCollectionManager,
|
||||
|
||||
@@ -6,11 +6,11 @@ import type {
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '../../../ui/button';
|
||||
import { FlexWrapper } from '../../../ui/layout';
|
||||
import { Menu } from '../../../ui/menu';
|
||||
import { CreateFilterMenu } from '../filter/vars';
|
||||
import type { useCollectionManager } from '../use-collection-manager';
|
||||
import * as styles from './collection-list.css';
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DeleteIcon, EditIcon, FilterIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
Menu,
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
type MenuItemProps,
|
||||
} from '@toeverything/components/menu';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
@@ -14,6 +8,7 @@ import {
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { Menu, MenuIcon, MenuItem, type MenuItemProps } from '../../../ui/menu';
|
||||
import type { useCollectionManager } from '../use-collection-manager';
|
||||
import type { AllPageListConfig } from '.';
|
||||
import * as styles from './collection-operations.css';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button } from '../../../ui/button';
|
||||
import Input from '../../../ui/input';
|
||||
import { Modal } from '../../../ui/modal';
|
||||
import * as styles from './create-collection.css';
|
||||
|
||||
export interface CreateCollectionModalProps {
|
||||
|
||||
+3
-3
@@ -2,11 +2,11 @@ import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { RadioButton, RadioButtonGroup } from '../../../../ui/button';
|
||||
import { RadioButton, RadioButtonGroup } from '../../../../index';
|
||||
import { Button } from '../../../../ui/button';
|
||||
import { Modal } from '../../../../ui/modal';
|
||||
import * as styles from './edit-collection.css';
|
||||
import { PagesMode } from './pages-mode';
|
||||
import { RulesMode } from './rules-mode';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Modal } from '../../../../ui/modal';
|
||||
import type { AllPageListConfig } from './edit-collection';
|
||||
import { SelectPage } from './select-page';
|
||||
export const useSelectPage = ({
|
||||
|
||||
+1
-1
@@ -2,10 +2,10 @@ import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import clsx from 'clsx';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
|
||||
import { Menu } from '../../../../ui/menu';
|
||||
import { FilterList } from '../../filter/filter-list';
|
||||
import { VariableSelect } from '../../filter/vars';
|
||||
import { VirtualizedPageList } from '../../virtualized-page-list';
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '../../../../ui/button';
|
||||
import { Menu } from '../../../../ui/menu';
|
||||
import { FilterList } from '../../filter';
|
||||
import { VariableSelect } from '../../filter/vars';
|
||||
import { VirtualizedPageList } from '../../virtualized-page-list';
|
||||
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SaveIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Button } from '../../../ui/button';
|
||||
import { createEmptyCollection } from '../use-collection-manager';
|
||||
import { useEditCollectionName } from './use-edit-collection';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import bytes from 'bytes';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { Tooltip } from '../../ui/tooltip';
|
||||
import * as styles from './share.css';
|
||||
|
||||
export interface StorageProgressProgress {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/// <reference types="../../type.d.ts" />
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { Modal, type ModalProps } from '@toeverything/components/modal';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Modal, type ModalProps } from '../../ui/modal';
|
||||
import editingVideo from './editingVideo.mp4';
|
||||
import {
|
||||
arrowStyle,
|
||||
|
||||
@@ -23,7 +23,7 @@ export const appStyle = style({
|
||||
inset: 0,
|
||||
opacity: 'var(--affine-noise-opacity, 0)',
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: '2.5%',
|
||||
backgroundSize: '3%',
|
||||
// todo: figure out how to use vanilla-extract webpack plugin to inject img url
|
||||
backgroundImage: `var(--noise-background)`,
|
||||
},
|
||||
@@ -32,13 +32,13 @@ export const appStyle = style({
|
||||
|
||||
globalStyle(`html[data-theme="light"] ${appStyle}`, {
|
||||
vars: {
|
||||
'--affine-noise-opacity': '0.25',
|
||||
'--affine-noise-opacity': '0.35',
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`html[data-theme="dark"] ${appStyle}`, {
|
||||
vars: {
|
||||
'--affine-noise-opacity': '0.1',
|
||||
'--affine-noise-opacity': '1',
|
||||
},
|
||||
|
||||
'@media': {
|
||||
@@ -54,13 +54,11 @@ export const mainContainerStyle = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
maxWidth: '100%',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
selectors: {
|
||||
'&[data-show-padding="true"]': {
|
||||
margin: '8px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
'@media': {
|
||||
print: {
|
||||
overflow: 'visible',
|
||||
@@ -72,12 +70,6 @@ export const mainContainerStyle = style({
|
||||
'&[data-show-padding="true"][data-is-macos="true"]': {
|
||||
borderRadius: '6px',
|
||||
},
|
||||
'&[data-in-trash-page="true"]': {
|
||||
marginBottom: '66px',
|
||||
},
|
||||
'&[data-in-trash-page="true"][data-show-padding="true"]': {
|
||||
marginBottom: '66px',
|
||||
},
|
||||
'&[data-show-padding="true"]:before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
@@ -124,7 +116,7 @@ globalStyle(
|
||||
);
|
||||
|
||||
export const toolStyle = style({
|
||||
position: 'fixed',
|
||||
position: 'absolute',
|
||||
right: '30px',
|
||||
bottom: '30px',
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
@@ -143,20 +135,4 @@ export const toolStyle = style({
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
'&[data-in-trash-page="true"]': {
|
||||
bottom: '70px',
|
||||
'@media': {
|
||||
'screen and (max-width: 960px)': {
|
||||
bottom: '80px',
|
||||
},
|
||||
'screen and (max-width: 640px)': {
|
||||
bottom: '85px',
|
||||
},
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -35,14 +35,13 @@ export const AppContainer = ({
|
||||
export interface MainContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
padding?: boolean;
|
||||
inTrashPage?: boolean;
|
||||
}
|
||||
|
||||
export const MainContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<MainContainerProps>
|
||||
>(function MainContainer(
|
||||
{ className, padding, inTrashPage, children, ...props },
|
||||
{ className, padding, children, ...props },
|
||||
ref
|
||||
): ReactElement {
|
||||
return (
|
||||
@@ -51,7 +50,6 @@ export const MainContainer = forwardRef<
|
||||
className={clsx(mainContainerStyle, className)}
|
||||
data-is-macos={environment.isDesktop && environment.isMacOs}
|
||||
data-show-padding={!!padding}
|
||||
data-in-trash-page={!!inTrashPage}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
@@ -61,14 +59,8 @@ export const MainContainer = forwardRef<
|
||||
|
||||
MainContainer.displayName = 'MainContainer';
|
||||
|
||||
export const ToolContainer = (
|
||||
props: PropsWithChildren & { inTrashPage: boolean }
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div className={toolStyle} data-in-trash-page={!!props.inTrashPage}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
export const ToolContainer = (props: PropsWithChildren): ReactElement => {
|
||||
return <div className={toolStyle}>{props.children}</div>;
|
||||
};
|
||||
|
||||
export const WorkspaceFallback = (): ReactElement => {
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
// TODO: Check `input` , `loading`, not migrated from `design`
|
||||
export * from './styles';
|
||||
export * from './ui/avatar';
|
||||
export * from './ui/button';
|
||||
export * from './ui/checkbox';
|
||||
export * from './ui/divider';
|
||||
export * from './ui/empty';
|
||||
export * from './ui/input';
|
||||
export * from './ui/layout';
|
||||
export * from './ui/loading';
|
||||
export * from './ui/lottie/collections-icon';
|
||||
export * from './ui/lottie/delete-icon';
|
||||
export * from './ui/menu';
|
||||
export * from './ui/modal';
|
||||
export * from './ui/popover';
|
||||
export * from './ui/scrollbar';
|
||||
export * from './ui/skeleton';
|
||||
export * from './ui/switch';
|
||||
export * from './ui/table';
|
||||
export * from './ui/toast';
|
||||
export * from './ui/tooltip';
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--noise-background: url(./noise.png);
|
||||
--noise-background: url(./noise.avif);
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -174,6 +174,7 @@ legend {
|
||||
border: 0;
|
||||
font-size: var(--affine-font-base);
|
||||
font-family: var(--affine-font-family);
|
||||
font-feature-settings: 'calt' 0;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
@@ -11,8 +11,12 @@ globalStyle('html', {
|
||||
vars: lightCssVariables,
|
||||
});
|
||||
|
||||
globalStyle('html[data-theme="dark"]', {
|
||||
vars: darkCssVariables,
|
||||
globalStyle('html', {
|
||||
'@media': {
|
||||
'(prefers-color-scheme: dark)': {
|
||||
vars: darkCssVariables,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { CameraIcon } from '@blocksuite/icons';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { Avatar, type AvatarProps } from './avatar';
|
||||
|
||||
export default {
|
||||
title: 'UI/Avatar',
|
||||
component: Avatar,
|
||||
argTypes: {
|
||||
onClick: () => console.log('Click button'),
|
||||
},
|
||||
} satisfies Meta<AvatarProps>;
|
||||
|
||||
const Template: StoryFn<AvatarProps> = args => <Avatar {...args} />;
|
||||
|
||||
export const DefaultAvatar: StoryFn<AvatarProps> = Template.bind(undefined);
|
||||
DefaultAvatar.args = {
|
||||
name: 'AFFiNE',
|
||||
url: 'https://affine.pro/favicon-96.png',
|
||||
size: 50,
|
||||
};
|
||||
export const Fallback: StoryFn<AvatarProps> = Template.bind(undefined);
|
||||
Fallback.args = {
|
||||
name: 'AFFiNE',
|
||||
size: 50,
|
||||
};
|
||||
export const ColorfulFallback: StoryFn<AvatarProps> = Template.bind(undefined);
|
||||
ColorfulFallback.args = {
|
||||
size: 50,
|
||||
colorfulFallback: true,
|
||||
name: 'blocksuite',
|
||||
};
|
||||
export const WithHover: StoryFn<AvatarProps> = Template.bind(undefined);
|
||||
WithHover.args = {
|
||||
size: 50,
|
||||
colorfulFallback: true,
|
||||
name: 'With Hover',
|
||||
hoverIcon: <CameraIcon />,
|
||||
};
|
||||
|
||||
export const WithRemove: StoryFn<AvatarProps> = Template.bind(undefined);
|
||||
WithRemove.args = {
|
||||
size: 50,
|
||||
colorfulFallback: true,
|
||||
name: 'With Hover',
|
||||
hoverIcon: <CameraIcon />,
|
||||
removeTooltipOptions: { content: 'This is remove tooltip' },
|
||||
avatarTooltipOptions: { content: 'This is avatar tooltip' },
|
||||
onRemove: e => {
|
||||
console.log('on remove', e);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
type AvatarFallbackProps,
|
||||
type AvatarImageProps,
|
||||
type AvatarProps as RadixAvatarProps,
|
||||
Fallback as AvatarFallback,
|
||||
Image as AvatarImage,
|
||||
Root as AvatarRoot,
|
||||
} from '@radix-ui/react-avatar';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import type { CSSProperties, HTMLAttributes, MouseEvent } from 'react';
|
||||
import { forwardRef, type ReactElement, useMemo, useState } from 'react';
|
||||
|
||||
import { IconButton } from '../button';
|
||||
import { Tooltip, type TooltipProps } from '../tooltip';
|
||||
import { ColorfulFallback } from './colorful-fallback';
|
||||
import * as style from './style.css';
|
||||
import { sizeVar } from './style.css';
|
||||
|
||||
export type AvatarProps = {
|
||||
size?: number;
|
||||
url?: string | null;
|
||||
name?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
colorfulFallback?: boolean;
|
||||
hoverIcon?: ReactElement;
|
||||
onRemove?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
avatarTooltipOptions?: Omit<TooltipProps, 'children'>;
|
||||
removeTooltipOptions?: Omit<TooltipProps, 'children'>;
|
||||
|
||||
fallbackProps?: AvatarFallbackProps;
|
||||
imageProps?: Omit<AvatarImageProps, 'src'>;
|
||||
avatarProps?: RadixAvatarProps;
|
||||
hoverWrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||
removeButtonProps?: HTMLAttributes<HTMLButtonElement>;
|
||||
} & HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(
|
||||
(
|
||||
{
|
||||
size = 20,
|
||||
style: propsStyles = {},
|
||||
url,
|
||||
name,
|
||||
className,
|
||||
colorfulFallback = false,
|
||||
hoverIcon,
|
||||
fallbackProps: { className: fallbackClassName, ...fallbackProps } = {},
|
||||
imageProps,
|
||||
avatarProps,
|
||||
onRemove,
|
||||
hoverWrapperProps: {
|
||||
className: hoverWrapperClassName,
|
||||
...hoverWrapperProps
|
||||
} = {},
|
||||
avatarTooltipOptions,
|
||||
removeTooltipOptions,
|
||||
removeButtonProps: {
|
||||
className: removeButtonClassName,
|
||||
...removeButtonProps
|
||||
} = {},
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const firstCharOfName = useMemo(() => {
|
||||
return name?.slice(0, 1) || 'A';
|
||||
}, [name]);
|
||||
const [imageDom, setImageDom] = useState<HTMLDivElement | null>(null);
|
||||
const [removeButtonDom, setRemoveButtonDom] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
|
||||
return (
|
||||
<AvatarRoot className={style.avatarRoot} {...avatarProps} ref={ref}>
|
||||
<Tooltip
|
||||
portalOptions={{ container: imageDom }}
|
||||
{...avatarTooltipOptions}
|
||||
>
|
||||
<div
|
||||
ref={setImageDom}
|
||||
className={clsx(style.avatarWrapper, className)}
|
||||
style={{
|
||||
...assignInlineVars({
|
||||
[sizeVar]: size ? `${size}px` : '20px',
|
||||
}),
|
||||
...propsStyles,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<AvatarImage
|
||||
className={style.avatarImage}
|
||||
src={url || ''}
|
||||
alt={name}
|
||||
{...imageProps}
|
||||
/>
|
||||
|
||||
<AvatarFallback
|
||||
className={clsx(style.avatarFallback, fallbackClassName)}
|
||||
delayMs={url ? 600 : undefined}
|
||||
{...fallbackProps}
|
||||
>
|
||||
{colorfulFallback ? (
|
||||
<ColorfulFallback char={firstCharOfName} />
|
||||
) : (
|
||||
firstCharOfName
|
||||
)}
|
||||
</AvatarFallback>
|
||||
{hoverIcon ? (
|
||||
<div
|
||||
className={clsx(style.hoverWrapper, hoverWrapperClassName)}
|
||||
{...hoverWrapperProps}
|
||||
>
|
||||
{hoverIcon}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{onRemove ? (
|
||||
<Tooltip
|
||||
portalOptions={{ container: removeButtonDom }}
|
||||
{...removeTooltipOptions}
|
||||
>
|
||||
<IconButton
|
||||
size="extraSmall"
|
||||
type="default"
|
||||
className={clsx(style.removeButton, removeButtonClassName)}
|
||||
onClick={onRemove}
|
||||
ref={setRemoveButtonDom}
|
||||
{...removeButtonProps}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</AvatarRoot>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Avatar.displayName = 'Avatar';
|
||||
@@ -0,0 +1,67 @@
|
||||
import clsx from 'clsx';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
DefaultAvatarBottomItemStyle,
|
||||
DefaultAvatarBottomItemWithAnimationStyle,
|
||||
DefaultAvatarContainerStyle,
|
||||
DefaultAvatarMiddleItemStyle,
|
||||
DefaultAvatarMiddleItemWithAnimationStyle,
|
||||
DefaultAvatarTopItemStyle,
|
||||
} from './style.css';
|
||||
|
||||
const colorsSchema = [
|
||||
['#FF0000', '#FF00E5', '#FFAE73'],
|
||||
['#FF5C00', '#FFC700', '#FFE073'],
|
||||
['#FFDA16', '#FFFBA6', '#FFBE73'],
|
||||
['#8CD317', '#FCFF5C', '#67CAE9'],
|
||||
['#28E19F', '#89FFC6', '#39A880'],
|
||||
['#35B7E0', '#77FFCE', '#5076FF'],
|
||||
['#3D39FF', '#77BEFF', '#3502FF'],
|
||||
['#BD08EB', '#755FFF', '#6967E4'],
|
||||
];
|
||||
|
||||
export const ColorfulFallback = ({ char }: { char: string }) => {
|
||||
const colors = useMemo(() => {
|
||||
const index = char.toUpperCase().charCodeAt(0);
|
||||
return colorsSchema[index % colorsSchema.length];
|
||||
}, [char]);
|
||||
|
||||
const timer = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const [topColor, middleColor, bottomColor] = colors;
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={DefaultAvatarContainerStyle}
|
||||
onMouseEnter={() => {
|
||||
timer.current = setTimeout(() => {
|
||||
setIsHover(true);
|
||||
}, 300);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
clearTimeout(timer.current);
|
||||
setIsHover(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={DefaultAvatarTopItemStyle}
|
||||
style={{ background: bottomColor }}
|
||||
></div>
|
||||
<div
|
||||
className={clsx(DefaultAvatarMiddleItemStyle, {
|
||||
[DefaultAvatarMiddleItemWithAnimationStyle]: isHover,
|
||||
})}
|
||||
style={{ background: middleColor }}
|
||||
></div>
|
||||
<div
|
||||
className={clsx(DefaultAvatarBottomItemStyle, {
|
||||
[DefaultAvatarBottomItemWithAnimationStyle]: isHover,
|
||||
})}
|
||||
style={{ background: topColor }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ColorfulFallback;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './avatar';
|
||||
@@ -0,0 +1,210 @@
|
||||
import { createVar, globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
export const sizeVar = createVar('sizeVar');
|
||||
|
||||
const bottomAnimation = keyframes({
|
||||
'0%': {
|
||||
top: '-44%',
|
||||
left: '-11%',
|
||||
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
|
||||
},
|
||||
'16%': {
|
||||
left: '-18%',
|
||||
top: '-51%',
|
||||
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
|
||||
},
|
||||
'32%': {
|
||||
left: '-7%',
|
||||
top: '-40%',
|
||||
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
|
||||
},
|
||||
'48%': {
|
||||
left: '-15%',
|
||||
top: '-39%',
|
||||
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
|
||||
},
|
||||
'64%': {
|
||||
left: '-7%',
|
||||
top: '-40%',
|
||||
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
|
||||
},
|
||||
'80%': {
|
||||
left: '-18%',
|
||||
top: '-51%',
|
||||
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
|
||||
},
|
||||
'100%': {
|
||||
top: '-44%',
|
||||
left: '-11%',
|
||||
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
|
||||
},
|
||||
});
|
||||
const middleAnimation = keyframes({
|
||||
'0%': {
|
||||
left: '-30px',
|
||||
top: '-30px',
|
||||
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
|
||||
},
|
||||
'16%': {
|
||||
left: '-37px',
|
||||
top: '-37px',
|
||||
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
|
||||
},
|
||||
'32%': {
|
||||
left: '-20px',
|
||||
top: '-10px',
|
||||
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
|
||||
},
|
||||
'48%': {
|
||||
left: '-27px',
|
||||
top: '-2px',
|
||||
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
|
||||
},
|
||||
'64%': {
|
||||
left: '-20px',
|
||||
top: '-10px',
|
||||
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
|
||||
},
|
||||
'80%': {
|
||||
left: '-37px',
|
||||
top: '-37px',
|
||||
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
|
||||
},
|
||||
'100%': {
|
||||
left: '-30px',
|
||||
top: '-30px',
|
||||
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
|
||||
},
|
||||
});
|
||||
|
||||
export const DefaultAvatarContainerStyle = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const DefaultAvatarMiddleItemStyle = style({
|
||||
width: '83%',
|
||||
height: '81%',
|
||||
position: 'absolute',
|
||||
left: '-30%',
|
||||
top: '-30%',
|
||||
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
|
||||
opacity: '0.8',
|
||||
filter: 'blur(12px)',
|
||||
transformOrigin: 'center center',
|
||||
animation: `${middleAnimation} 3s ease-in-out forwards infinite`,
|
||||
animationPlayState: 'paused',
|
||||
});
|
||||
export const DefaultAvatarMiddleItemWithAnimationStyle = style({
|
||||
animationPlayState: 'running',
|
||||
});
|
||||
export const DefaultAvatarBottomItemStyle = style({
|
||||
width: '98%',
|
||||
height: '97%',
|
||||
position: 'absolute',
|
||||
top: '-44%',
|
||||
left: '-11%',
|
||||
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
|
||||
opacity: '0.8',
|
||||
filter: 'blur(12px)',
|
||||
transformOrigin: 'center center',
|
||||
willChange: 'left, top, transform',
|
||||
animation: `${bottomAnimation} 3s ease-in-out forwards infinite`,
|
||||
animationPlayState: 'paused',
|
||||
});
|
||||
export const DefaultAvatarBottomItemWithAnimationStyle = style({
|
||||
animationPlayState: 'running',
|
||||
});
|
||||
export const DefaultAvatarTopItemStyle = style({
|
||||
width: '104%',
|
||||
height: '94%',
|
||||
position: 'absolute',
|
||||
right: '-30%',
|
||||
top: '-30%',
|
||||
opacity: '0.8',
|
||||
filter: 'blur(12px)',
|
||||
transform: 'matrix(-0.28, -0.96, 0.93, -0.37, 0, 0)',
|
||||
transformOrigin: 'center center',
|
||||
});
|
||||
|
||||
export const avatarRoot = style({
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
flexShrink: 0,
|
||||
});
|
||||
export const avatarWrapper = style({
|
||||
vars: {
|
||||
[sizeVar]: 'unset',
|
||||
},
|
||||
width: sizeVar,
|
||||
height: sizeVar,
|
||||
fontSize: `calc(${sizeVar} / 2)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
verticalAlign: 'middle',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const avatarImage = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '50%',
|
||||
});
|
||||
|
||||
export const avatarFallback = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'var(--affine-primary-color)',
|
||||
color: 'var(--affine-white)',
|
||||
lineHeight: '1',
|
||||
fontWeight: '500',
|
||||
});
|
||||
|
||||
export const hoverWrapper = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(60, 61, 63, 0.5)',
|
||||
zIndex: '1',
|
||||
color: 'var(--affine-white)',
|
||||
opacity: 0,
|
||||
transition: 'opacity .15s',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const removeButton = style({
|
||||
position: 'absolute',
|
||||
right: '-8px',
|
||||
top: '-2px',
|
||||
visibility: 'hidden',
|
||||
zIndex: '1',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: '#f6f6f6',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${avatarRoot}:hover ${removeButton}`, {
|
||||
visibility: 'visible',
|
||||
});
|
||||
globalStyle(`${avatarRoot} ${removeButton}:hover`, {
|
||||
background: '#f6f6f6',
|
||||
});
|
||||
@@ -0,0 +1,374 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const button = style({
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
touchAction: 'manipulation',
|
||||
flexShrink: 0,
|
||||
outline: '0',
|
||||
border: '1px solid',
|
||||
padding: '0 18px',
|
||||
borderRadius: '8px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 500,
|
||||
transition: 'all .3s',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
cursor: 'pointer',
|
||||
|
||||
// changeable
|
||||
height: '28px',
|
||||
background: 'var(--affine-white)',
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
|
||||
selectors: {
|
||||
'&.text-bold': {
|
||||
fontWeight: 600,
|
||||
},
|
||||
'&:not(.without-hover):hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
'&.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
color: 'var(--affine-disable-color)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&.loading': {
|
||||
cursor: 'default',
|
||||
color: 'var(--affine-disable-color)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover':
|
||||
{
|
||||
background: 'inherit',
|
||||
},
|
||||
|
||||
'&.block': { display: 'flex', width: '100%' },
|
||||
|
||||
'&.circle': {
|
||||
borderRadius: '50%',
|
||||
},
|
||||
'&.round': {
|
||||
borderRadius: '14px',
|
||||
},
|
||||
// size
|
||||
'&.large': {
|
||||
height: '32px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
},
|
||||
'&.round.large': {
|
||||
borderRadius: '16px',
|
||||
},
|
||||
'&.extraLarge': {
|
||||
height: '40px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 700,
|
||||
},
|
||||
'&.extraLarge.primary': {
|
||||
boxShadow: 'var(--affine-large-button-effect) !important',
|
||||
},
|
||||
'&.round.extraLarge': {
|
||||
borderRadius: '20px',
|
||||
},
|
||||
|
||||
// type
|
||||
'&.plain': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
borderColor: 'transparent',
|
||||
background: 'transparent',
|
||||
},
|
||||
|
||||
'&.primary': {
|
||||
color: 'var(--affine-pure-white)',
|
||||
background: 'var(--affine-primary-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||
},
|
||||
'&.primary:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-primary-color)',
|
||||
},
|
||||
'&.primary.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.primary.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-primary-color)',
|
||||
},
|
||||
|
||||
'&.error': {
|
||||
color: 'var(--affine-pure-white)',
|
||||
background: 'var(--affine-error-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||
},
|
||||
'&.error:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-error-color)',
|
||||
},
|
||||
'&.error.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.error.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-error-color)',
|
||||
},
|
||||
|
||||
'&.warning': {
|
||||
color: 'var(--affine-pure-white)',
|
||||
background: 'var(--affine-warning-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||
},
|
||||
'&.warning:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-warning-color)',
|
||||
},
|
||||
'&.warning.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.warning.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-warning-color)',
|
||||
},
|
||||
|
||||
'&.success': {
|
||||
color: 'var(--affine-pure-white)',
|
||||
background: 'var(--affine-success-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||
},
|
||||
'&.success:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-success-color)',
|
||||
},
|
||||
'&.success.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.success.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-success-color)',
|
||||
},
|
||||
|
||||
'&.processing': {
|
||||
color: 'var(--affine-pure-white)',
|
||||
background: 'var(--affine-processing-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||
},
|
||||
'&.processing:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-processing-color)',
|
||||
},
|
||||
'&.processing.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.processing.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-processing-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${button} > span`, {
|
||||
// flex: 1,
|
||||
lineHeight: 1,
|
||||
padding: '0 4px',
|
||||
});
|
||||
|
||||
export const buttonIcon = style({
|
||||
flexShrink: 0,
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '16px',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
selectors: {
|
||||
'&.start': {
|
||||
marginRight: '4px',
|
||||
},
|
||||
'&.end': {
|
||||
marginLeft: '4px',
|
||||
},
|
||||
'&.large': {
|
||||
fontSize: '20px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
},
|
||||
'&.extraLarge': {
|
||||
fontSize: '20px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
},
|
||||
'&.color-white': {
|
||||
color: 'var(--affine-pure-white)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const iconButton = style({
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
touchAction: 'manipulation',
|
||||
outline: '0',
|
||||
border: '1px solid',
|
||||
borderRadius: '4px',
|
||||
transition: 'all .3s',
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
cursor: 'pointer',
|
||||
background: 'var(--affine-white)',
|
||||
|
||||
// changeable
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
fontSize: '20px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
selectors: {
|
||||
'&.without-padding': {
|
||||
margin: '-2px',
|
||||
},
|
||||
'&.active': {
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
|
||||
'&:not(.without-hover):hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
'&.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
color: 'var(--affine-disable-color)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&.loading': {
|
||||
cursor: 'default',
|
||||
color: 'var(--affine-disable-color)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover':
|
||||
{
|
||||
background: 'inherit',
|
||||
},
|
||||
|
||||
// size
|
||||
'&.large': {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
fontSize: '24px',
|
||||
},
|
||||
'&.large.without-padding': {
|
||||
margin: '-4px',
|
||||
},
|
||||
'&.small': { width: '20px', height: '20px', fontSize: '16px' },
|
||||
'&.extra-small': { width: '16px', height: '16px', fontSize: '12px' },
|
||||
|
||||
// type
|
||||
'&.plain': {
|
||||
color: 'var(--affine-icon-color)',
|
||||
borderColor: 'transparent',
|
||||
background: 'transparent',
|
||||
},
|
||||
'&.plain.active': {
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
|
||||
'&.primary': {
|
||||
color: 'var(--affine-white)',
|
||||
background: 'var(--affine-primary-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||
},
|
||||
'&.primary:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-primary-color)',
|
||||
},
|
||||
'&.primary.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.primary.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-primary-color)',
|
||||
},
|
||||
|
||||
'&.error': {
|
||||
color: 'var(--affine-white)',
|
||||
background: 'var(--affine-error-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||
},
|
||||
'&.error:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-error-color)',
|
||||
},
|
||||
'&.error.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.error.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-error-color)',
|
||||
},
|
||||
|
||||
'&.warning': {
|
||||
color: 'var(--affine-white)',
|
||||
background: 'var(--affine-warning-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||
},
|
||||
'&.warning:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-warning-color)',
|
||||
},
|
||||
'&.warning.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.warning.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-warning-color)',
|
||||
},
|
||||
|
||||
'&.success': {
|
||||
color: 'var(--affine-white)',
|
||||
background: 'var(--affine-success-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||
},
|
||||
'&.success:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-success-color)',
|
||||
},
|
||||
'&.success.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.success.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-success-color)',
|
||||
},
|
||||
|
||||
'&.processing': {
|
||||
color: 'var(--affine-white)',
|
||||
background: 'var(--affine-processing-color)',
|
||||
borderColor: 'var(--affine-black-10)',
|
||||
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||
},
|
||||
'&.processing:not(.without-hover):hover': {
|
||||
background:
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-processing-color)',
|
||||
},
|
||||
'&.processing.disabled': {
|
||||
opacity: '.4',
|
||||
cursor: 'default',
|
||||
},
|
||||
'&.processing.disabled:not(.without-hover):hover': {
|
||||
background: 'var(--affine-processing-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { InformationIcon } from '@blocksuite/icons';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { Button, type ButtonProps } from './button';
|
||||
export default {
|
||||
title: 'UI/Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
onClick: () => console.log('Click button'),
|
||||
},
|
||||
} satisfies Meta<ButtonProps>;
|
||||
|
||||
const Template: StoryFn<ButtonProps> = args => <Button {...args} />;
|
||||
|
||||
export const Default: StoryFn<ButtonProps> = Template.bind(undefined);
|
||||
Default.args = {
|
||||
type: 'default',
|
||||
children: 'This is a default button',
|
||||
icon: <InformationIcon />,
|
||||
};
|
||||
|
||||
export const Primary: StoryFn<ButtonProps> = Template.bind(undefined);
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
children: 'Content',
|
||||
icon: <InformationIcon />,
|
||||
};
|
||||
|
||||
export const Disabled: StoryFn<ButtonProps> = Template.bind(undefined);
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
children: 'This is a disabled button',
|
||||
};
|
||||
|
||||
export const LargeSizeButton: StoryFn<ButtonProps> = Template.bind(undefined);
|
||||
LargeSizeButton.args = {
|
||||
size: 'large',
|
||||
children: 'This is a large button',
|
||||
};
|
||||
|
||||
export const ExtraLargeSizeButton: StoryFn<ButtonProps> =
|
||||
Template.bind(undefined);
|
||||
ExtraLargeSizeButton.args = {
|
||||
size: 'extraLarge',
|
||||
children: 'This is a extra large button',
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type FC,
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { Loading } from '../loading';
|
||||
import { button, buttonIcon } from './button.css';
|
||||
|
||||
export type ButtonType =
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'plain'
|
||||
| 'error'
|
||||
| 'warning'
|
||||
| 'success'
|
||||
| 'processing';
|
||||
export type ButtonSize = 'default' | 'large' | 'extraLarge';
|
||||
type BaseButtonProps = {
|
||||
type?: ButtonType;
|
||||
disabled?: boolean;
|
||||
icon?: ReactElement;
|
||||
iconPosition?: 'start' | 'end';
|
||||
shape?: 'default' | 'round' | 'circle';
|
||||
block?: boolean;
|
||||
size?: ButtonSize;
|
||||
loading?: boolean;
|
||||
withoutHoverStyle?: boolean;
|
||||
};
|
||||
|
||||
export type ButtonProps = PropsWithChildren<BaseButtonProps> &
|
||||
Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & {
|
||||
componentProps?: {
|
||||
startIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>;
|
||||
endIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>;
|
||||
};
|
||||
};
|
||||
|
||||
type IconButtonProps = PropsWithChildren<BaseButtonProps> &
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'type'>;
|
||||
|
||||
const defaultProps = {
|
||||
type: 'default',
|
||||
disabled: false,
|
||||
shape: 'default',
|
||||
size: 'default',
|
||||
iconPosition: 'start',
|
||||
loading: false,
|
||||
withoutHoverStyle: false,
|
||||
} as const;
|
||||
|
||||
const ButtonIcon: FC<IconButtonProps> = props => {
|
||||
const {
|
||||
size,
|
||||
icon,
|
||||
iconPosition = 'start',
|
||||
children,
|
||||
type,
|
||||
loading,
|
||||
withoutHoverStyle,
|
||||
...otherProps
|
||||
} = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
const onlyIcon = icon && !children;
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
className={clsx(buttonIcon, {
|
||||
'color-white': type !== 'default' && type !== 'plain',
|
||||
large: size === 'large',
|
||||
extraLarge: size === 'extraLarge',
|
||||
end: iconPosition === 'end' && !onlyIcon,
|
||||
start: iconPosition === 'start' && !onlyIcon,
|
||||
loading,
|
||||
})}
|
||||
data-without-hover={withoutHoverStyle}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
children,
|
||||
type,
|
||||
disabled,
|
||||
shape,
|
||||
size,
|
||||
icon: propsIcon,
|
||||
iconPosition,
|
||||
block,
|
||||
loading,
|
||||
withoutHoverStyle,
|
||||
className,
|
||||
...otherProps
|
||||
} = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
} satisfies ButtonProps;
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
return propsIcon;
|
||||
}, [propsIcon, loading]);
|
||||
|
||||
const baseIconButtonProps = useMemo(() => {
|
||||
return {
|
||||
size,
|
||||
iconPosition,
|
||||
icon,
|
||||
type,
|
||||
disabled,
|
||||
loading,
|
||||
} as const;
|
||||
}, [disabled, icon, iconPosition, loading, size, type]);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...otherProps}
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
button,
|
||||
{
|
||||
primary: type === 'primary',
|
||||
plain: type === 'plain',
|
||||
error: type === 'error',
|
||||
warning: type === 'warning',
|
||||
success: type === 'success',
|
||||
processing: type === 'processing',
|
||||
large: size === 'large',
|
||||
extraLarge: size === 'extraLarge',
|
||||
disabled,
|
||||
circle: shape === 'circle',
|
||||
round: shape === 'round',
|
||||
block,
|
||||
loading,
|
||||
'without-hover': withoutHoverStyle,
|
||||
},
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
{icon && iconPosition === 'start' ? (
|
||||
<ButtonIcon
|
||||
{...baseIconButtonProps}
|
||||
{...props.componentProps?.startIcon}
|
||||
icon={icon}
|
||||
iconPosition="start"
|
||||
/>
|
||||
) : null}
|
||||
<span>{children}</span>
|
||||
{icon && iconPosition === 'end' ? (
|
||||
<ButtonIcon
|
||||
{...baseIconButtonProps}
|
||||
{...props.componentProps?.endIcon}
|
||||
icon={icon}
|
||||
iconPosition="end"
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
export default Button;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { InformationIcon } from '@blocksuite/icons';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { IconButton, type IconButtonProps } from './icon-button';
|
||||
export default {
|
||||
title: 'UI/IconButton',
|
||||
component: IconButton,
|
||||
argTypes: {
|
||||
onClick: () => console.log('Click button'),
|
||||
},
|
||||
} satisfies Meta<IconButtonProps>;
|
||||
|
||||
const Template: StoryFn<IconButtonProps> = args => <IconButton {...args} />;
|
||||
|
||||
export const Plain: StoryFn<IconButtonProps> = Template.bind(undefined);
|
||||
Plain.args = {
|
||||
children: <InformationIcon />,
|
||||
};
|
||||
|
||||
export const Primary: StoryFn<IconButtonProps> = Template.bind(undefined);
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
icon: <InformationIcon />,
|
||||
};
|
||||
|
||||
export const Disabled: StoryFn<IconButtonProps> = Template.bind(undefined);
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
icon: <InformationIcon />,
|
||||
};
|
||||
export const ExtraSmallSizeButton: StoryFn<IconButtonProps> =
|
||||
Template.bind(undefined);
|
||||
ExtraSmallSizeButton.args = {
|
||||
size: 'extraSmall',
|
||||
icon: <InformationIcon />,
|
||||
};
|
||||
export const SmallSizeButton: StoryFn<IconButtonProps> =
|
||||
Template.bind(undefined);
|
||||
SmallSizeButton.args = {
|
||||
size: 'small',
|
||||
icon: <InformationIcon />,
|
||||
};
|
||||
export const LargeSizeButton: StoryFn<IconButtonProps> =
|
||||
Template.bind(undefined);
|
||||
LargeSizeButton.args = {
|
||||
size: 'large',
|
||||
icon: <InformationIcon />,
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
||||
import { forwardRef, type ReactElement } from 'react';
|
||||
|
||||
import { Loading } from '../loading';
|
||||
import type { ButtonType } from './button';
|
||||
import { iconButton } from './button.css';
|
||||
|
||||
export type IconButtonSize = 'default' | 'large' | 'small' | 'extraSmall';
|
||||
export type IconButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'type'> &
|
||||
PropsWithChildren<{
|
||||
type?: ButtonType;
|
||||
disabled?: boolean;
|
||||
size?: IconButtonSize;
|
||||
loading?: boolean;
|
||||
withoutPadding?: boolean;
|
||||
active?: boolean;
|
||||
withoutHoverStyle?: boolean;
|
||||
icon?: ReactElement;
|
||||
}>;
|
||||
|
||||
const defaultProps = {
|
||||
type: 'plain',
|
||||
disabled: false,
|
||||
size: 'default',
|
||||
loading: false,
|
||||
withoutPadding: false,
|
||||
active: false,
|
||||
withoutHoverStyle: false,
|
||||
} as const;
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
type,
|
||||
size,
|
||||
withoutPadding,
|
||||
children,
|
||||
disabled,
|
||||
loading,
|
||||
active,
|
||||
withoutHoverStyle,
|
||||
icon: propsIcon,
|
||||
className,
|
||||
...otherProps
|
||||
} = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
iconButton,
|
||||
{
|
||||
'without-padding': withoutPadding,
|
||||
|
||||
primary: type === 'primary',
|
||||
plain: type === 'plain',
|
||||
error: type === 'error',
|
||||
warning: type === 'warning',
|
||||
success: type === 'success',
|
||||
processing: type === 'processing',
|
||||
|
||||
large: size === 'large',
|
||||
small: size === 'small',
|
||||
'extra-small': size === 'extraSmall',
|
||||
|
||||
disabled,
|
||||
loading,
|
||||
active,
|
||||
'without-hover': withoutHoverStyle,
|
||||
},
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
data-disabled={disabled}
|
||||
{...otherProps}
|
||||
>
|
||||
{loading ? <Loading /> : children || propsIcon}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IconButton.displayName = 'IconButton';
|
||||
export default IconButton;
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './button';
|
||||
export * from './dropdown-button';
|
||||
export * from './icon-button';
|
||||
export * from './radio';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Checkbox } from './checkbox';
|
||||
|
||||
export default {
|
||||
title: 'UI/Checkbox',
|
||||
component: Checkbox,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta<typeof Checkbox>;
|
||||
|
||||
export const Basic: StoryFn<typeof Checkbox> = props => {
|
||||
const [checked, setChecked] = useState(props.checked);
|
||||
const handleChange = (
|
||||
_event: React.ChangeEvent<HTMLInputElement>,
|
||||
checked: boolean
|
||||
) => {
|
||||
setChecked(checked);
|
||||
props.onChange?.(_event, checked);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
style={{ fontSize: 14 }}
|
||||
{...props}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Checkbox
|
||||
style={{ fontSize: 16 }}
|
||||
{...props}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Checkbox
|
||||
style={{ fontSize: 18 }}
|
||||
{...props}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Checkbox
|
||||
style={{ fontSize: 24 }}
|
||||
{...props}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Basic.args = {
|
||||
checked: true,
|
||||
disabled: false,
|
||||
indeterminate: false,
|
||||
onChange: console.log,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { Divider, type DividerProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'UI/Divider',
|
||||
component: Divider,
|
||||
} satisfies Meta<typeof Divider>;
|
||||
|
||||
const Template: StoryFn<DividerProps> = args => (
|
||||
<div
|
||||
style={{
|
||||
height: '100px',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Divider {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Default: StoryFn<DividerProps> = Template.bind(undefined);
|
||||
Default.args = {};
|
||||
@@ -0,0 +1,51 @@
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
export type DividerOrientation = 'horizontal' | 'vertical';
|
||||
export type DividerProps = PropsWithChildren &
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'type'> & {
|
||||
orientation?: DividerOrientation;
|
||||
size?: 'thinner' | 'default';
|
||||
dividerColor?: string;
|
||||
};
|
||||
|
||||
export const Divider = forwardRef<HTMLDivElement, DividerProps>(
|
||||
(
|
||||
{
|
||||
orientation = 'horizontal',
|
||||
size = 'default',
|
||||
dividerColor = 'var(--affine-border-color)',
|
||||
style,
|
||||
className,
|
||||
...otherProps
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
styles.divider,
|
||||
{
|
||||
[styles.verticalDivider]: orientation === 'vertical',
|
||||
[styles.thinner]:
|
||||
size === 'thinner' && orientation === 'horizontal',
|
||||
[styles.verticalThinner]:
|
||||
size === 'thinner' && orientation === 'vertical',
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: dividerColor ? dividerColor : undefined,
|
||||
...style,
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Divider.displayName = 'Divider';
|
||||
export default Divider;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './divider';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const divider = style({
|
||||
height: '1px',
|
||||
backgroundColor: 'var(--affine-border-color)',
|
||||
borderRadius: '8px',
|
||||
margin: '8px 0',
|
||||
width: '100%',
|
||||
});
|
||||
export const thinner = style({
|
||||
height: '0.5px',
|
||||
});
|
||||
export const verticalDivider = style({
|
||||
width: '1px',
|
||||
borderRadius: '8px',
|
||||
height: '100%',
|
||||
margin: '0 2px',
|
||||
});
|
||||
export const verticalThinner = style({
|
||||
width: '0.5px',
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { Empty, type EmptyContentProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'UI/Empty',
|
||||
component: Empty,
|
||||
} satisfies Meta<typeof Empty>;
|
||||
|
||||
const Template: StoryFn<EmptyContentProps> = args => <Empty {...args} />;
|
||||
|
||||
export const Default: StoryFn<EmptyContentProps> = Template.bind(undefined);
|
||||
Default.args = {
|
||||
title: 'No Data',
|
||||
description: 'No Data',
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { InformationIcon } from '@blocksuite/icons';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { Input, type InputProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'UI/Input',
|
||||
component: Input,
|
||||
} satisfies Meta<typeof Input>;
|
||||
|
||||
const Template: StoryFn<InputProps> = args => (
|
||||
<div style={{ width: '50%' }}>
|
||||
<Input {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Default: StoryFn<InputProps> = Template.bind(undefined);
|
||||
Default.args = {
|
||||
defaultValue: 'This is a default input',
|
||||
};
|
||||
|
||||
export const WithPrefix: StoryFn<InputProps> = Template.bind(undefined);
|
||||
WithPrefix.args = {
|
||||
defaultValue: 'This is a input with prefix',
|
||||
preFix: <InformationIcon />,
|
||||
};
|
||||
|
||||
export const Large: StoryFn<InputProps> = Template.bind(undefined);
|
||||
Large.args = {
|
||||
placeholder: 'This is a large input',
|
||||
size: 'large',
|
||||
};
|
||||
export const ExtraLarge: StoryFn<InputProps> = Template.bind(undefined);
|
||||
ExtraLarge.args = {
|
||||
placeholder: 'This is a extraLarge input',
|
||||
size: 'extraLarge',
|
||||
};
|
||||
|
||||
export const CustomWidth: StoryFn<InputProps> = Template.bind(undefined);
|
||||
CustomWidth.args = {
|
||||
width: 300,
|
||||
placeholder: 'This is a custom width input, default is 100%',
|
||||
};
|
||||
export const ErrorStatus: StoryFn<InputProps> = Template.bind(undefined);
|
||||
ErrorStatus.args = {
|
||||
status: 'error',
|
||||
placeholder: 'This is a error status input',
|
||||
};
|
||||
export const WarningStatus: StoryFn<InputProps> = Template.bind(undefined);
|
||||
WarningStatus.args = {
|
||||
status: 'warning',
|
||||
placeholder: 'This is a warning status input',
|
||||
};
|
||||
export const Disabled: StoryFn<InputProps> = Template.bind(undefined);
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
placeholder: 'This is a disabled input',
|
||||
};
|
||||
@@ -8,17 +8,17 @@ export const inputWrapper = style({
|
||||
},
|
||||
width: widthVar,
|
||||
height: 28,
|
||||
padding: '4px 10px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
backgroundColor: 'var(--affine-white-10)',
|
||||
lineHeight: '22px',
|
||||
padding: '0 10px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
border: '1px solid',
|
||||
backgroundColor: 'var(--affine-white)',
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
// icon size
|
||||
fontSize: '16px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
boxSizing: 'border-box',
|
||||
|
||||
selectors: {
|
||||
'&.no-border': {
|
||||
@@ -27,14 +27,10 @@ export const inputWrapper = style({
|
||||
// size
|
||||
'&.large': {
|
||||
height: 32,
|
||||
// icon size
|
||||
fontSize: '20px',
|
||||
},
|
||||
'&.extra-large': {
|
||||
height: 40,
|
||||
padding: '8px 10px',
|
||||
// icon size
|
||||
fontSize: '20px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
// color
|
||||
'&.disabled': {
|
||||
@@ -49,45 +45,34 @@ export const inputWrapper = style({
|
||||
'&.warning': {
|
||||
borderColor: 'var(--affine-warning-color)',
|
||||
},
|
||||
'&.default': {
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
},
|
||||
'&.default.focus': {
|
||||
borderColor: 'var(--affine-primary-color)',
|
||||
boxShadow: 'var(--affine-active-shadow)',
|
||||
boxShadow: '0px 0px 0px 2px rgba(30, 150, 235, 0.30);',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const input = style({
|
||||
height: '100%',
|
||||
width: '0',
|
||||
flex: 1,
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
fontWeight: '500',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
boxSizing: 'border-box',
|
||||
// prevent default style
|
||||
WebkitAppearance: 'none',
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
|
||||
selectors: {
|
||||
'&::placeholder': {
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
},
|
||||
'&:autofill, &:-webkit-autofill, &:-internal-autofill-selected, &:-webkit-autofill:hover, &:-webkit-autofill:focus, &:-webkit-autofill:active':
|
||||
{
|
||||
// The reason for using ‘!important’ here is:
|
||||
// The user agent style sheets of many browsers utilise !important in their :-webkit-autofill style declarations.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/:autofill#:~:text=%2C%20254)-,!important,-%3B%0Abackground%2Dimage
|
||||
backgroundColor: 'var(--affine-white-10) !important',
|
||||
['-webkit-box-shadow' as string]: 'none !important',
|
||||
},
|
||||
'&:disabled': {
|
||||
color: 'var(--affine-text-disable-color)',
|
||||
},
|
||||
'&.large, &.extra-large': {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: '24px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { Loading, type LoadingProps } from './loading';
|
||||
|
||||
export default {
|
||||
title: 'UI/Loading',
|
||||
component: Loading,
|
||||
} satisfies Meta<typeof Loading>;
|
||||
|
||||
const Template: StoryFn<LoadingProps> = args => <Loading {...args} />;
|
||||
|
||||
export const Default: StoryFn<LoadingProps> = Template.bind(undefined);
|
||||
Default.args = {};
|
||||
@@ -11,20 +11,25 @@ export const Loading = ({ size, speed = 1.2 }: LoadingProps) => {
|
||||
return (
|
||||
<svg
|
||||
className={loading}
|
||||
viewBox="0 0 1024 1024"
|
||||
focusable="false"
|
||||
data-icon="loading"
|
||||
width={size ? `${size}px` : '.8em'}
|
||||
height={size ? `${size}px` : '.8em'}
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
width={size ? `${size}px` : '16px'}
|
||||
height={size ? `${size}px` : '16px'}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{
|
||||
...assignInlineVars({
|
||||
[speedVar]: `${speed}s`,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path>
|
||||
<path
|
||||
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM4.95017 12C4.95017 15.8935 8.10648 19.0498 12 19.0498C15.8935 19.0498 19.0498 15.8935 19.0498 12C19.0498 8.10648 15.8935 4.95017 12 4.95017C8.10648 4.95017 4.95017 8.10648 4.95017 12Z"
|
||||
fill="var(--affine-black-10)"
|
||||
/>
|
||||
<path
|
||||
d="M20.525 12C21.3396 12 22.0111 11.3361 21.8914 10.5303C21.7714 9.72269 21.5527 8.93094 21.2388 8.17317C20.7362 6.95991 19.9997 5.85752 19.0711 4.92893C18.1425 4.00035 17.0401 3.26375 15.8268 2.7612C15.0691 2.44732 14.2773 2.22859 13.4697 2.10859C12.6639 1.98886 12 2.66038 12 3.475C12 4.28962 12.6674 4.93455 13.4643 5.10374C13.8853 5.19314 14.2983 5.32113 14.6979 5.48665C15.5533 5.84095 16.3304 6.36024 16.9851 7.0149C17.6398 7.66955 18.1591 8.44674 18.5133 9.30208C18.6789 9.70167 18.8069 10.1147 18.8963 10.5357C19.0655 11.3326 19.7104 12 20.525 12Z"
|
||||
fill="var(--affine-primary-color)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import {
|
||||
AnimatedCollectionsIcon,
|
||||
type CollectionsIconProps,
|
||||
} from './collections-icon';
|
||||
|
||||
export default {
|
||||
title: 'UI/Lottie/Collection Icons',
|
||||
component: AnimatedCollectionsIcon,
|
||||
} satisfies Meta<typeof AnimatedCollectionsIcon>;
|
||||
|
||||
const Template: StoryFn<CollectionsIconProps> = args => (
|
||||
<AnimatedCollectionsIcon {...args} />
|
||||
);
|
||||
|
||||
export const Default: StoryFn<CollectionsIconProps> = Template.bind(undefined);
|
||||
Default.args = {};
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import { AnimatedDeleteIcon, type DeleteIconProps } from './delete-icon';
|
||||
|
||||
export default {
|
||||
title: 'UI/Lottie/Delete Icon',
|
||||
component: AnimatedDeleteIcon,
|
||||
} satisfies Meta<typeof AnimatedDeleteIcon>;
|
||||
|
||||
const Template: StoryFn<DeleteIconProps> = args => (
|
||||
<AnimatedDeleteIcon {...args} />
|
||||
);
|
||||
|
||||
export const Default: StoryFn<DeleteIconProps> = Template.bind(undefined);
|
||||
Default.args = {};
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './menu';
|
||||
export * from './menu.types';
|
||||
export * from './menu-icon';
|
||||
export * from './menu-item';
|
||||
export * from './menu-separator';
|
||||
export * from './menu-sub';
|
||||
export * from './menu-trigger';
|
||||
@@ -0,0 +1,39 @@
|
||||
import clsx from 'clsx';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { forwardRef, type HTMLAttributes, useMemo } from 'react';
|
||||
|
||||
import { menuItemIcon } from './styles.css';
|
||||
|
||||
export interface MenuIconProps
|
||||
extends PropsWithChildren,
|
||||
HTMLAttributes<HTMLDivElement> {
|
||||
icon?: ReactNode;
|
||||
position?: 'start' | 'end';
|
||||
}
|
||||
|
||||
export const MenuIcon = forwardRef<HTMLDivElement, MenuIconProps>(
|
||||
({ children, icon, position = 'start', className, ...otherProps }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
menuItemIcon,
|
||||
{
|
||||
end: position === 'end',
|
||||
start: position === 'start',
|
||||
},
|
||||
className
|
||||
),
|
||||
[className, position]
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
{icon || children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
MenuIcon.displayName = 'MenuIcon';
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
|
||||
import type { MenuItemProps } from './menu.types';
|
||||
import { useMenuItem } from './use-menu-item';
|
||||
|
||||
export const MenuItem = ({
|
||||
children: propsChildren,
|
||||
type = 'default',
|
||||
className: propsClassName,
|
||||
preFix,
|
||||
endFix,
|
||||
checked,
|
||||
selected,
|
||||
block,
|
||||
...otherProps
|
||||
}: MenuItemProps) => {
|
||||
const { className, children } = useMenuItem({
|
||||
children: propsChildren,
|
||||
className: propsClassName,
|
||||
type,
|
||||
preFix,
|
||||
endFix,
|
||||
checked,
|
||||
selected,
|
||||
block,
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item className={className} {...otherProps}>
|
||||
{children}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user