mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
@@ -39,6 +39,10 @@ export interface ModalProps extends DialogProps {
|
|||||||
* @default 'fadeScaleTop'
|
* @default 'fadeScaleTop'
|
||||||
*/
|
*/
|
||||||
animation?: 'fadeScaleTop' | 'none' | 'slideBottom';
|
animation?: 'fadeScaleTop' | 'none' | 'slideBottom';
|
||||||
|
/**
|
||||||
|
* Whether to show the modal in full screen mode
|
||||||
|
*/
|
||||||
|
fullScreen?: boolean;
|
||||||
}
|
}
|
||||||
type PointerDownOutsideEvent = Parameters<
|
type PointerDownOutsideEvent = Parameters<
|
||||||
Exclude<DialogContentProps['onPointerDownOutside'], undefined>
|
Exclude<DialogContentProps['onPointerDownOutside'], undefined>
|
||||||
@@ -144,6 +148,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
animation = environment.isBrowser && environment.isMobile
|
animation = environment.isBrowser && environment.isMobile
|
||||||
? 'slideBottom'
|
? 'slideBottom'
|
||||||
: 'fadeScaleTop',
|
: 'fadeScaleTop',
|
||||||
|
fullScreen,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
const { className: closeButtonClassName, ...otherCloseButtonProps } =
|
const { className: closeButtonClassName, ...otherCloseButtonProps } =
|
||||||
@@ -209,6 +214,7 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
{...otherOverlayOptions}
|
{...otherOverlayOptions}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
data-full-screen={fullScreen}
|
||||||
data-modal={modal}
|
data-modal={modal}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
`anim-${animation}`,
|
`anim-${animation}`,
|
||||||
@@ -223,8 +229,14 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
className={clsx(styles.modalContent, contentClassName)}
|
className={clsx(styles.modalContent, contentClassName)}
|
||||||
style={{
|
style={{
|
||||||
...assignInlineVars({
|
...assignInlineVars({
|
||||||
[styles.widthVar]: getVar(width, '50vw'),
|
[styles.widthVar]: getVar(
|
||||||
[styles.heightVar]: getVar(height, 'unset'),
|
width,
|
||||||
|
fullScreen ? '100dvw' : '50dvw'
|
||||||
|
),
|
||||||
|
[styles.heightVar]: getVar(
|
||||||
|
height,
|
||||||
|
fullScreen ? '100dvh' : 'unset'
|
||||||
|
),
|
||||||
[styles.minHeightVar]: getVar(minHeight, '26px'),
|
[styles.minHeightVar]: getVar(minHeight, '26px'),
|
||||||
}),
|
}),
|
||||||
...contentStyle,
|
...contentStyle,
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ export const modalContentWrapper = style({
|
|||||||
},
|
},
|
||||||
|
|
||||||
selectors: {
|
selectors: {
|
||||||
|
'&[data-full-screen="true"]': {
|
||||||
|
padding: '0 !important',
|
||||||
|
},
|
||||||
'&.anim-none': {
|
'&.anim-none': {
|
||||||
animation: 'none',
|
animation: 'none',
|
||||||
},
|
},
|
||||||
@@ -136,6 +139,19 @@ export const modalContent = style({
|
|||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
// :focus-visible will set outline
|
// :focus-visible will set outline
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'[data-full-screen="true"] &': {
|
||||||
|
vars: {
|
||||||
|
[widthVar]: '100vw',
|
||||||
|
[heightVar]: '100vh',
|
||||||
|
[minHeightVar]: '100vh',
|
||||||
|
},
|
||||||
|
maxWidth: '100vw',
|
||||||
|
maxHeight: '100vh',
|
||||||
|
borderRadius: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export const closeButton = style({
|
export const closeButton = style({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import type { SettingProps } from '../components/affine/setting-modal';
|
|||||||
import type { ActiveTab } from '../components/affine/setting-modal/types';
|
import type { ActiveTab } from '../components/affine/setting-modal/types';
|
||||||
// modal atoms
|
// modal atoms
|
||||||
export const openWorkspacesModalAtom = atom(false);
|
export const openWorkspacesModalAtom = atom(false);
|
||||||
|
/**
|
||||||
|
* @deprecated use `useSignOut` hook instated
|
||||||
|
*/
|
||||||
export const openSignOutModalAtom = atom(false);
|
export const openSignOutModalAtom = atom(false);
|
||||||
export const openQuotaModalAtom = atom(false);
|
export const openQuotaModalAtom = atom(false);
|
||||||
export const openStarAFFiNEModalAtom = atom(false);
|
export const openStarAFFiNEModalAtom = atom(false);
|
||||||
|
|||||||
@@ -17,30 +17,30 @@ import { DateFormatSetting } from './date-format-setting';
|
|||||||
import { settingWrapper } from './style.css';
|
import { settingWrapper } from './style.css';
|
||||||
import { ThemeEditorSetting } from './theme-editor-setting';
|
import { ThemeEditorSetting } from './theme-editor-setting';
|
||||||
|
|
||||||
|
export const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
value: 'system',
|
||||||
|
label: t['com.affine.themeSettings.system'](),
|
||||||
|
testId: 'system-theme-trigger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'light',
|
||||||
|
label: t['com.affine.themeSettings.light'](),
|
||||||
|
testId: 'light-theme-trigger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'dark',
|
||||||
|
label: t['com.affine.themeSettings.dark'](),
|
||||||
|
testId: 'dark-theme-trigger',
|
||||||
|
},
|
||||||
|
] satisfies RadioItem[];
|
||||||
|
|
||||||
export const ThemeSettings = () => {
|
export const ThemeSettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme } = useTheme();
|
||||||
|
|
||||||
const radioItems = useMemo<RadioItem[]>(
|
const radioItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
|
||||||
() => [
|
|
||||||
{
|
|
||||||
value: 'system',
|
|
||||||
label: t['com.affine.themeSettings.system'](),
|
|
||||||
testId: 'system-theme-trigger',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'light',
|
|
||||||
label: t['com.affine.themeSettings.light'](),
|
|
||||||
testId: 'light-theme-trigger',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'dark',
|
|
||||||
label: t['com.affine.themeSettings.dark'](),
|
|
||||||
testId: 'dark-theme-trigger',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
|
|||||||
@@ -41,51 +41,67 @@ import { Virtuoso } from 'react-virtuoso';
|
|||||||
import { DropdownMenu } from './menu';
|
import { DropdownMenu } from './menu';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
|
const getLabel = (fontKey: FontFamily, t: ReturnType<typeof useI18n>) => {
|
||||||
|
switch (fontKey) {
|
||||||
|
case 'Sans':
|
||||||
|
return t['com.affine.appearanceSettings.fontStyle.sans']();
|
||||||
|
case 'Serif':
|
||||||
|
return t['com.affine.appearanceSettings.fontStyle.serif']();
|
||||||
|
case 'Mono':
|
||||||
|
return t[`com.affine.appearanceSettings.fontStyle.mono`]();
|
||||||
|
case 'Custom':
|
||||||
|
return t['com.affine.settings.editorSettings.edgeless.custom']();
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBaseFontStyleOptions = (
|
||||||
|
t: ReturnType<typeof useI18n>
|
||||||
|
): Array<Omit<RadioItem, 'value'> & { value: FontFamily }> => {
|
||||||
|
return fontStyleOptions
|
||||||
|
.map(({ key, value }) => {
|
||||||
|
if (key === 'Custom') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const label = getLabel(key, t);
|
||||||
|
return {
|
||||||
|
value: key,
|
||||||
|
label,
|
||||||
|
testId: 'system-font-style-trigger',
|
||||||
|
style: {
|
||||||
|
fontFamily: value,
|
||||||
|
},
|
||||||
|
} satisfies RadioItem;
|
||||||
|
})
|
||||||
|
.filter(item => item !== null);
|
||||||
|
};
|
||||||
|
|
||||||
const FontFamilySettings = () => {
|
const FontFamilySettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { editorSettingService } = useServices({ EditorSettingService });
|
const { editorSettingService } = useServices({ EditorSettingService });
|
||||||
const settings = useLiveData(editorSettingService.editorSetting.settings$);
|
const settings = useLiveData(editorSettingService.editorSetting.settings$);
|
||||||
|
|
||||||
const getLabel = useCallback(
|
|
||||||
(fontKey: FontFamily) => {
|
|
||||||
switch (fontKey) {
|
|
||||||
case 'Sans':
|
|
||||||
return t['com.affine.appearanceSettings.fontStyle.sans']();
|
|
||||||
case 'Serif':
|
|
||||||
return t['com.affine.appearanceSettings.fontStyle.serif']();
|
|
||||||
case 'Mono':
|
|
||||||
return t[`com.affine.appearanceSettings.fontStyle.mono`]();
|
|
||||||
case 'Custom':
|
|
||||||
return t['com.affine.settings.editorSettings.edgeless.custom']();
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
const radioItems = useMemo(() => {
|
const radioItems = useMemo(() => {
|
||||||
return fontStyleOptions
|
const items = getBaseFontStyleOptions(t);
|
||||||
.map(({ key, value }) => {
|
if (!environment.isDesktop) return items;
|
||||||
if (key === 'Custom' && !environment.isDesktop) {
|
|
||||||
return null;
|
// resolve custom fonts
|
||||||
}
|
const customOption = fontStyleOptions.find(opt => opt.key === 'Custom');
|
||||||
const label = getLabel(key);
|
if (customOption) {
|
||||||
let fontFamily = value;
|
const fontFamily = settings.customFontFamily
|
||||||
if (key === 'Custom' && settings.customFontFamily) {
|
? `${settings.customFontFamily}, ${customOption.value}`
|
||||||
fontFamily = `${settings.customFontFamily}, ${value}`;
|
: customOption.value;
|
||||||
}
|
items.push({
|
||||||
return {
|
value: customOption.key,
|
||||||
value: key,
|
label: getLabel(customOption.key, t),
|
||||||
label,
|
testId: 'system-font-style-trigger',
|
||||||
testId: 'system-font-style-trigger',
|
style: { fontFamily },
|
||||||
style: {
|
});
|
||||||
fontFamily,
|
}
|
||||||
},
|
|
||||||
} satisfies RadioItem;
|
return items;
|
||||||
})
|
}, [settings.customFontFamily, t]);
|
||||||
.filter(item => item !== null);
|
|
||||||
}, [getLabel, settings.customFontFamily]);
|
|
||||||
|
|
||||||
const handleFontFamilyChange = useCallback(
|
const handleFontFamilyChange = useCallback(
|
||||||
(value: FontFamily) => {
|
(value: FontFamily) => {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ type SignOutConfirmModalI18NKeys =
|
|||||||
| 'cancel'
|
| 'cancel'
|
||||||
| 'confirm';
|
| 'confirm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `useSignOut` instead
|
||||||
|
*/
|
||||||
export const SignOutModal = ({ ...props }: ConfirmModalProps) => {
|
export const SignOutModal = ({ ...props }: ConfirmModalProps) => {
|
||||||
const { title, description, cancelText, confirmText } = props;
|
const { title, description, cancelText, confirmText } = props;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|||||||
114
packages/frontend/core/src/hooks/affine/use-sign-out.ts
Normal file
114
packages/frontend/core/src/hooks/affine/use-sign-out.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
type ConfirmModalProps,
|
||||||
|
notify,
|
||||||
|
useConfirmModal,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import {
|
||||||
|
GlobalContextService,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
WorkspacesService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useNavigateHelper } from '../use-navigate-helper';
|
||||||
|
|
||||||
|
type SignOutConfirmModalI18NKeys =
|
||||||
|
| 'title'
|
||||||
|
| 'description'
|
||||||
|
| 'cancel'
|
||||||
|
| 'confirm';
|
||||||
|
|
||||||
|
export const useSignOut = ({
|
||||||
|
onConfirm,
|
||||||
|
confirmButtonOptions,
|
||||||
|
contentOptions,
|
||||||
|
...props
|
||||||
|
}: ConfirmModalProps = {}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
const globalContextService = useService(GlobalContextService);
|
||||||
|
|
||||||
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
|
const currentWorkspaceId = useLiveData(
|
||||||
|
globalContextService.globalContext.workspaceId.$
|
||||||
|
);
|
||||||
|
const currentWorkspaceMetadata = useLiveData(
|
||||||
|
currentWorkspaceId
|
||||||
|
? workspacesService.list.workspace$(currentWorkspaceId)
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const signOut = useCallback(async () => {
|
||||||
|
onConfirm?.()?.catch(console.error);
|
||||||
|
try {
|
||||||
|
await authService.signOut();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
// TODO(@eyhn): i18n
|
||||||
|
notify.error({
|
||||||
|
title: 'Failed to sign out',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if current workspace is affine cloud, switch to local workspace
|
||||||
|
if (currentWorkspaceMetadata?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||||
|
const localWorkspace = workspaces.find(
|
||||||
|
w => w.flavour === WorkspaceFlavour.LOCAL
|
||||||
|
);
|
||||||
|
if (localWorkspace) {
|
||||||
|
openPage(localWorkspace.id, WorkspaceSubPath.ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
authService,
|
||||||
|
currentWorkspaceMetadata?.flavour,
|
||||||
|
onConfirm,
|
||||||
|
openPage,
|
||||||
|
workspaces,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getDefaultText = useCallback(
|
||||||
|
(key: SignOutConfirmModalI18NKeys) => {
|
||||||
|
return t[`com.affine.auth.sign-out.confirm-modal.${key}`]();
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmSignOut = useCallback(() => {
|
||||||
|
openConfirmModal({
|
||||||
|
title: getDefaultText('title'),
|
||||||
|
description: getDefaultText('description'),
|
||||||
|
cancelText: getDefaultText('cancel'),
|
||||||
|
confirmText: getDefaultText('confirm'),
|
||||||
|
confirmButtonOptions: {
|
||||||
|
...confirmButtonOptions,
|
||||||
|
variant: 'error',
|
||||||
|
['data-testid' as string]: 'confirm-sign-out-button',
|
||||||
|
},
|
||||||
|
contentOptions: {
|
||||||
|
...contentOptions,
|
||||||
|
['data-testid' as string]: 'confirm-sign-out-modal',
|
||||||
|
},
|
||||||
|
onConfirm: signOut,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
confirmButtonOptions,
|
||||||
|
contentOptions,
|
||||||
|
getDefaultText,
|
||||||
|
openConfirmModal,
|
||||||
|
props,
|
||||||
|
signOut,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return confirmSignOut;
|
||||||
|
};
|
||||||
@@ -1567,5 +1567,18 @@
|
|||||||
"com.affine.import-template.dialog.createDocToWorkspace": "Create doc to \"{{workspace}}\"",
|
"com.affine.import-template.dialog.createDocToWorkspace": "Create doc to \"{{workspace}}\"",
|
||||||
"com.affine.import-template.dialog.createDocToNewWorkspace": "Create into a New Workspace",
|
"com.affine.import-template.dialog.createDocToNewWorkspace": "Create into a New Workspace",
|
||||||
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template",
|
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template",
|
||||||
|
"com.affine.mobile.setting.header-title": "Settings",
|
||||||
|
"com.affine.mobile.setting.appearance.title": "Appearance",
|
||||||
|
"com.affine.mobile.setting.appearance.theme": "Color mode",
|
||||||
|
"com.affine.mobile.setting.appearance.font": "Font style",
|
||||||
|
"com.affine.mobile.setting.appearance.language": "Display language",
|
||||||
|
"com.affine.mobile.setting.about.title": "About",
|
||||||
|
"com.affine.mobile.setting.about.appVersion": "App version",
|
||||||
|
"com.affine.mobile.setting.about.editorVersion": "Editor version",
|
||||||
|
"com.affine.mobile.setting.others.title": "Privacy & others",
|
||||||
|
"com.affine.mobile.setting.others.github": "Star us on GitHub",
|
||||||
|
"com.affine.mobile.setting.others.website": "Official website",
|
||||||
|
"com.affine.mobile.setting.others.privacy": "Privacy",
|
||||||
|
"com.affine.mobile.setting.others.terms": "Terms of use",
|
||||||
"com.affine.mobile.search.empty": "No results found"
|
"com.affine.mobile.search.empty": "No results found"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,12 @@
|
|||||||
"core-js": "^3.36.1",
|
"core-js": "^3.36.1",
|
||||||
"figma-squircle": "^0.3.1",
|
"figma-squircle": "^0.3.1",
|
||||||
"intl-segmenter-polyfill-rs": "^0.1.7",
|
"intl-segmenter-polyfill-rs": "^0.1.7",
|
||||||
|
"jotai": "^2.9.3",
|
||||||
|
"jotai-devtools": "^0.10.1",
|
||||||
|
"jotai-effect": "^1.0.2",
|
||||||
|
"jotai-scope": "^0.7.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.26.1"
|
"react-router-dom": "^6.26.1"
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export * from './doc-card';
|
|||||||
export * from './page-header';
|
export * from './page-header';
|
||||||
export * from './search-input';
|
export * from './search-input';
|
||||||
export * from './search-result';
|
export * from './search-result';
|
||||||
|
export * from './user-plan-tag';
|
||||||
export * from './workspace-selector';
|
export * from './workspace-selector';
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export interface PageHeaderProps
|
|||||||
* whether to show back button
|
* whether to show back button
|
||||||
*/
|
*/
|
||||||
back?: boolean;
|
back?: boolean;
|
||||||
|
/**
|
||||||
|
* Override back button action
|
||||||
|
*/
|
||||||
|
backAction?: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* prefix content, shown after back button(if exists)
|
* prefix content, shown after back button(if exists)
|
||||||
@@ -42,6 +46,7 @@ export const PageHeader = forwardRef<HTMLHeadElement, PageHeaderProps>(
|
|||||||
function PageHeader(
|
function PageHeader(
|
||||||
{
|
{
|
||||||
back,
|
back,
|
||||||
|
backAction,
|
||||||
prefix,
|
prefix,
|
||||||
suffix,
|
suffix,
|
||||||
children,
|
children,
|
||||||
@@ -56,8 +61,8 @@ export const PageHeader = forwardRef<HTMLHeadElement, PageHeaderProps>(
|
|||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const handleRouteBack = useCallback(() => {
|
const handleRouteBack = useCallback(() => {
|
||||||
history.back();
|
backAction ? backAction() : history.back();
|
||||||
}, []);
|
}, [backAction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
ServerConfigService,
|
||||||
|
SubscriptionService,
|
||||||
|
} from '@affine/core/modules/cloud';
|
||||||
|
import { SubscriptionPlan } from '@affine/graphql';
|
||||||
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { forwardRef, type HTMLProps, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { tag } from './style.css';
|
||||||
|
|
||||||
|
export const UserPlanTag = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
HTMLProps<HTMLDivElement>
|
||||||
|
>(function UserPlanTag({ className, ...attrs }, ref) {
|
||||||
|
const { serverConfigService, subscriptionService } = useServices({
|
||||||
|
ServerConfigService,
|
||||||
|
SubscriptionService,
|
||||||
|
});
|
||||||
|
const hasPayment = useLiveData(
|
||||||
|
serverConfigService.serverConfig.features$.map(r => r?.payment)
|
||||||
|
);
|
||||||
|
const plan = useLiveData(
|
||||||
|
subscriptionService.subscription.pro$.map(subscription =>
|
||||||
|
subscription !== null ? subscription?.plan : null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const isBeliever = useLiveData(subscriptionService.subscription.isBeliever$);
|
||||||
|
const isLoading = plan === null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// revalidate subscription to get the latest status
|
||||||
|
subscriptionService.subscription.revalidate();
|
||||||
|
}, [subscriptionService]);
|
||||||
|
|
||||||
|
if (!hasPayment) return null;
|
||||||
|
|
||||||
|
if (isLoading) return null;
|
||||||
|
|
||||||
|
const planLabel = isBeliever ? 'Believer' : (plan ?? SubscriptionPlan.Free);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(tag, className)}
|
||||||
|
data-is-believer={isBeliever}
|
||||||
|
{...attrs}
|
||||||
|
>
|
||||||
|
{planLabel}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const tag = style({
|
||||||
|
display: 'flex',
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
height: 20,
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: cssVar('pureWhite'),
|
||||||
|
backgroundColor: cssVar('brandColor'),
|
||||||
|
padding: '0 4px',
|
||||||
|
borderRadius: 4,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[data-is-believer="true"]': {
|
||||||
|
backgroundColor: '#374151',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||||
import { WorkspaceLayoutProviders } from '@affine/core/layouts/workspace-layout';
|
import { WorkspaceLayoutProviders } from '@affine/core/layouts/workspace-layout';
|
||||||
import { CurrentWorkspaceModals } from '@affine/core/providers/modal-provider';
|
|
||||||
import { SWRConfigProvider } from '@affine/core/providers/swr-config-provider';
|
import { SWRConfigProvider } from '@affine/core/providers/swr-config-provider';
|
||||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +17,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { MobileCurrentWorkspaceModals } from '../../provider/model-provider';
|
||||||
|
|
||||||
export const WorkspaceLayout = ({
|
export const WorkspaceLayout = ({
|
||||||
meta,
|
meta,
|
||||||
children,
|
children,
|
||||||
@@ -78,7 +79,7 @@ export const WorkspaceLayout = ({
|
|||||||
<FrameworkScope scope={workspace.scope}>
|
<FrameworkScope scope={workspace.scope}>
|
||||||
<AffineErrorBoundary height="100vh">
|
<AffineErrorBoundary height="100vh">
|
||||||
<SWRConfigProvider>
|
<SWRConfigProvider>
|
||||||
<CurrentWorkspaceModals />
|
<MobileCurrentWorkspaceModals />
|
||||||
<WorkspaceLayoutProviders>{children}</WorkspaceLayoutProviders>
|
<WorkspaceLayoutProviders>{children}</WorkspaceLayoutProviders>
|
||||||
</SWRConfigProvider>
|
</SWRConfigProvider>
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
|
|||||||
60
packages/frontend/mobile/src/provider/model-provider.tsx
Normal file
60
packages/frontend/mobile/src/provider/model-provider.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { AiLoginRequiredModal } from '@affine/core/components/affine/auth/ai-login-required';
|
||||||
|
import { HistoryTipsModal } from '@affine/core/components/affine/history-tips-modal';
|
||||||
|
import { IssueFeedbackModal } from '@affine/core/components/affine/issue-feedback-modal';
|
||||||
|
import {
|
||||||
|
CloudQuotaModal,
|
||||||
|
LocalQuotaModal,
|
||||||
|
} from '@affine/core/components/affine/quota-reached-modal';
|
||||||
|
import { StarAFFiNEModal } from '@affine/core/components/affine/star-affine-modal';
|
||||||
|
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||||
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
|
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { MobileSettingModal } from '../views';
|
||||||
|
|
||||||
|
export function MobileCurrentWorkspaceModals() {
|
||||||
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
|
||||||
|
const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper(
|
||||||
|
currentWorkspace.docCollection
|
||||||
|
);
|
||||||
|
const deletePageTitles = trashModal.pageTitles;
|
||||||
|
const trashConfirmOpen = trashModal.open;
|
||||||
|
const onTrashConfirmOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
setTrashModal({
|
||||||
|
...trashModal,
|
||||||
|
open,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[trashModal, setTrashModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StarAFFiNEModal />
|
||||||
|
<IssueFeedbackModal />
|
||||||
|
{currentWorkspace ? <MobileSettingModal /> : null}
|
||||||
|
{currentWorkspace?.flavour === WorkspaceFlavour.LOCAL && (
|
||||||
|
<>
|
||||||
|
<LocalQuotaModal />
|
||||||
|
<HistoryTipsModal />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
|
||||||
|
<CloudQuotaModal />
|
||||||
|
)}
|
||||||
|
<AiLoginRequiredModal />
|
||||||
|
<PeekViewManagerModal />
|
||||||
|
<MoveToTrash.ConfirmModal
|
||||||
|
open={trashConfirmOpen}
|
||||||
|
onConfirm={handleOnConfirm}
|
||||||
|
onOpenChange={onTrashConfirmOpenChange}
|
||||||
|
titles={deletePageTitles}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { IconButton } from '@affine/component';
|
||||||
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { SettingsIcon } from '@blocksuite/icons/rc';
|
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { SearchInput, WorkspaceSelector } from '../../components';
|
import { SearchInput, WorkspaceSelector } from '../../components';
|
||||||
import { useGlobalEvent } from '../../hooks/use-global-events';
|
import { useGlobalEvent } from '../../hooks/use-global-events';
|
||||||
@@ -20,6 +21,7 @@ import * as styles from './styles.css';
|
|||||||
export const HomeHeader = () => {
|
export const HomeHeader = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const openSetting = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
const [dense, setDense] = useState(false);
|
const [dense, setDense] = useState(false);
|
||||||
|
|
||||||
@@ -49,13 +51,14 @@ export const HomeHeader = () => {
|
|||||||
<WorkspaceSelector />
|
<WorkspaceSelector />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.settingWrapper}>
|
<div className={styles.settingWrapper}>
|
||||||
<Link to="/settings">
|
<IconButton
|
||||||
<IconButton
|
onClick={() => {
|
||||||
size="24"
|
openSetting({ open: true, activeTab: 'appearance' });
|
||||||
style={{ padding: 10 }}
|
}}
|
||||||
icon={<SettingsIcon />}
|
size="24"
|
||||||
/>
|
style={{ padding: 10 }}
|
||||||
</Link>
|
icon={<SettingsIcon />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.searchWrapper}>
|
<div className={styles.searchWrapper}>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './all-docs';
|
export * from './all-docs';
|
||||||
export * from './home-header';
|
export * from './home-header';
|
||||||
export * from './recent-docs';
|
export * from './recent-docs';
|
||||||
|
export * from './settings';
|
||||||
|
|||||||
22
packages/frontend/mobile/src/views/settings/about/index.tsx
Normal file
22
packages/frontend/mobile/src/views/settings/about/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
|
||||||
|
import { SettingGroup } from '../group';
|
||||||
|
import { RowLayout } from '../row.layout';
|
||||||
|
|
||||||
|
const { appVersion, editorVersion } = runtimeConfig;
|
||||||
|
|
||||||
|
export const AboutGroup = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup title={t['com.affine.mobile.setting.about.title']()}>
|
||||||
|
<RowLayout label={t['com.affine.mobile.setting.about.appVersion']()}>
|
||||||
|
{appVersion}
|
||||||
|
</RowLayout>
|
||||||
|
|
||||||
|
<RowLayout label={t['com.affine.mobile.setting.about.editorVersion']()}>
|
||||||
|
{editorVersion}
|
||||||
|
</RowLayout>
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { getBaseFontStyleOptions } from '@affine/core/components/affine/setting-modal/general-setting/editor/general';
|
||||||
|
import {
|
||||||
|
EditorSettingService,
|
||||||
|
type FontFamily,
|
||||||
|
} from '@affine/core/modules/editor-settting';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { SettingDropdownSelect } from '../dropdown-select';
|
||||||
|
import { RowLayout } from '../row.layout';
|
||||||
|
|
||||||
|
export const FontStyleSetting = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||||
|
const settings = useLiveData(editorSetting.settings$);
|
||||||
|
|
||||||
|
const options = useMemo(() => getBaseFontStyleOptions(t), [t]);
|
||||||
|
const handleEdit = useCallback(
|
||||||
|
(v: FontFamily) => {
|
||||||
|
editorSetting.set('fontFamily', v);
|
||||||
|
},
|
||||||
|
[editorSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowLayout label={t['com.affine.mobile.setting.appearance.font']()}>
|
||||||
|
<SettingDropdownSelect<FontFamily>
|
||||||
|
options={options}
|
||||||
|
value={settings.fontFamily}
|
||||||
|
onChange={handleEdit}
|
||||||
|
/>
|
||||||
|
</RowLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
|
||||||
|
import { SettingGroup } from '../group';
|
||||||
|
import { FontStyleSetting } from './font';
|
||||||
|
import { LanguageSetting } from './language';
|
||||||
|
import { ThemeSetting } from './theme';
|
||||||
|
|
||||||
|
export const AppearanceGroup = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
return (
|
||||||
|
<SettingGroup title={t['com.affine.mobile.setting.appearance.title']()}>
|
||||||
|
<ThemeSetting />
|
||||||
|
<FontStyleSetting />
|
||||||
|
<LanguageSetting />
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useLanguageHelper } from '@affine/core/hooks/affine/use-language-helper';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { SettingDropdownSelect } from '../dropdown-select';
|
||||||
|
import { RowLayout } from '../row.layout';
|
||||||
|
|
||||||
|
export const LanguageSetting = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
const { currentLanguage, languagesList, onLanguageChange } =
|
||||||
|
useLanguageHelper();
|
||||||
|
|
||||||
|
const languageOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
languagesList.map(language => ({
|
||||||
|
label: language.originalName,
|
||||||
|
value: language.tag,
|
||||||
|
})),
|
||||||
|
[languagesList]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowLayout label={t['com.affine.mobile.setting.appearance.language']()}>
|
||||||
|
<SettingDropdownSelect
|
||||||
|
options={languageOptions}
|
||||||
|
value={currentLanguage?.tag}
|
||||||
|
onChange={onLanguageChange}
|
||||||
|
menuOptions={{
|
||||||
|
contentOptions: {
|
||||||
|
style: {
|
||||||
|
maxHeight: '60dvh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RowLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { getThemeOptions } from '@affine/core/components/affine/setting-modal/general-setting/appearance';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { SettingDropdownSelect } from '../dropdown-select';
|
||||||
|
import { RowLayout } from '../row.layout';
|
||||||
|
|
||||||
|
export const ThemeSetting = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const options = useMemo(() => getThemeOptions(t), [t]);
|
||||||
|
const { setTheme, theme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowLayout label={t['com.affine.mobile.setting.appearance.theme']()}>
|
||||||
|
<SettingDropdownSelect
|
||||||
|
options={options}
|
||||||
|
value={theme}
|
||||||
|
onChange={setTheme}
|
||||||
|
/>
|
||||||
|
</RowLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const root = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
export const label = style({
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: '22px',
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: -0.43,
|
||||||
|
color: cssVarV2('text/placeholder'),
|
||||||
|
});
|
||||||
|
export const icon = style({
|
||||||
|
fontSize: 24,
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { type MenuProps, MobileMenu, MobileMenuItem } from '@affine/component';
|
||||||
|
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
type HTMLAttributes,
|
||||||
|
type ReactNode,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import * as styles from './dropdown-select.css';
|
||||||
|
|
||||||
|
interface DropdownItem<V extends string> {
|
||||||
|
label?: ReactNode;
|
||||||
|
value: V;
|
||||||
|
testId?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
export interface SettingDropdownSelectProps<
|
||||||
|
V extends string,
|
||||||
|
E extends boolean | undefined,
|
||||||
|
> extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||||
|
options?: Array<DropdownItem<V>>;
|
||||||
|
value?: V;
|
||||||
|
onChange?: (
|
||||||
|
v: E extends true ? DropdownItem<V>['value'] : DropdownItem<V>
|
||||||
|
) => void;
|
||||||
|
emitValue?: E;
|
||||||
|
menuOptions?: Omit<MenuProps, 'items' | 'children'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingDropdownSelect = <
|
||||||
|
V extends string = string,
|
||||||
|
E extends boolean | undefined = true,
|
||||||
|
>({
|
||||||
|
options = [],
|
||||||
|
value,
|
||||||
|
emitValue = true,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
menuOptions,
|
||||||
|
...attrs
|
||||||
|
}: SettingDropdownSelectProps<V, E>) => {
|
||||||
|
const selectedItem = useMemo(
|
||||||
|
() => options.find(opt => opt.value === value),
|
||||||
|
[options, value]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<MobileMenu
|
||||||
|
items={options.map(opt => (
|
||||||
|
<MobileMenuItem
|
||||||
|
key={opt.value}
|
||||||
|
selected={value === opt.value}
|
||||||
|
data-testid={opt.testId}
|
||||||
|
onSelect={() =>
|
||||||
|
emitValue ? onChange?.(opt.value as any) : onChange?.(opt as any)
|
||||||
|
}
|
||||||
|
style={opt.style}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</MobileMenuItem>
|
||||||
|
))}
|
||||||
|
{...menuOptions}
|
||||||
|
>
|
||||||
|
<div className={clsx(styles.root, className)} {...attrs}>
|
||||||
|
<span className={styles.label}>{selectedItem?.label ?? ''}</span>
|
||||||
|
|
||||||
|
<ArrowDownSmallIcon className={styles.icon} />
|
||||||
|
</div>
|
||||||
|
</MobileMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
27
packages/frontend/mobile/src/views/settings/group.css.ts
Normal file
27
packages/frontend/mobile/src/views/settings/group.css.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const group = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
padding: '0px 8px',
|
||||||
|
color: cssVarV2('text/tertiary'),
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: '18px',
|
||||||
|
letterSpacing: -0.08,
|
||||||
|
fontWeight: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const content = style({
|
||||||
|
background: cssVarV2('layer/background/primary'),
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: '10px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
35
packages/frontend/mobile/src/views/settings/group.tsx
Normal file
35
packages/frontend/mobile/src/views/settings/group.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
forwardRef,
|
||||||
|
type HTMLProps,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import * as styles from './group.css';
|
||||||
|
|
||||||
|
export interface SettingGroupProps
|
||||||
|
extends Omit<HTMLProps<HTMLDivElement>, 'title'> {
|
||||||
|
title?: ReactNode;
|
||||||
|
contentClassName?: string;
|
||||||
|
contentStyle?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(
|
||||||
|
function SettingGroup(
|
||||||
|
{ children, title, className, contentClassName, contentStyle, ...attrs },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className={clsx(styles.group, className)} ref={ref} {...attrs}>
|
||||||
|
{title ? <h6 className={styles.title}>{title}</h6> : null}
|
||||||
|
<div
|
||||||
|
className={clsx(styles.content, contentClassName)}
|
||||||
|
style={contentStyle}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
70
packages/frontend/mobile/src/views/settings/index.tsx
Normal file
70
packages/frontend/mobile/src/views/settings/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Modal } from '@affine/component';
|
||||||
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { PageHeader } from '../../components';
|
||||||
|
import { AboutGroup } from './about';
|
||||||
|
import { AppearanceGroup } from './appearance';
|
||||||
|
import { OthersGroup } from './others';
|
||||||
|
import * as styles from './style.css';
|
||||||
|
import { UserProfile } from './user-profile';
|
||||||
|
import { UserUsage } from './user-usage';
|
||||||
|
|
||||||
|
export const MobileSettingModal = () => {
|
||||||
|
const [{ open }, setOpen] = useAtom(openSettingModalAtom);
|
||||||
|
|
||||||
|
const onOpenChange = useCallback(
|
||||||
|
(open: boolean) => setOpen(prev => ({ ...prev, open })),
|
||||||
|
[setOpen]
|
||||||
|
);
|
||||||
|
const closeModal = useCallback(() => onOpenChange(false), [onOpenChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
fullScreen
|
||||||
|
animation="slideBottom"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
contentOptions={{
|
||||||
|
style: {
|
||||||
|
padding: 0,
|
||||||
|
overflowY: 'auto',
|
||||||
|
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
withoutCloseButton
|
||||||
|
>
|
||||||
|
<MobileSetting onClose={closeModal} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileSetting = ({ onClose }: { onClose: () => void }) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const session = useService(AuthService).session;
|
||||||
|
|
||||||
|
useEffect(() => session.revalidate(), [session]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader back backAction={onClose}>
|
||||||
|
<span className={styles.pageTitle}>
|
||||||
|
{t['com.affine.mobile.setting.header-title']()}
|
||||||
|
</span>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div className={styles.root}>
|
||||||
|
<UserProfile />
|
||||||
|
<UserUsage />
|
||||||
|
<AppearanceGroup />
|
||||||
|
<AboutGroup />
|
||||||
|
<OthersGroup />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
packages/frontend/mobile/src/views/settings/others/index.tsx
Normal file
32
packages/frontend/mobile/src/views/settings/others/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
|
||||||
|
import { SettingGroup } from '../group';
|
||||||
|
import { RowLayout } from '../row.layout';
|
||||||
|
|
||||||
|
export const OthersGroup = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup title={t['com.affine.mobile.setting.others.title']()}>
|
||||||
|
<RowLayout
|
||||||
|
label={t['com.affine.mobile.setting.others.github']()}
|
||||||
|
href="https://github.com/toeverything/AFFiNE"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RowLayout
|
||||||
|
label={t['com.affine.mobile.setting.others.website']()}
|
||||||
|
href="https://affine.pro/"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RowLayout
|
||||||
|
label={t['com.affine.mobile.setting.others.privacy']()}
|
||||||
|
href="https://affine.pro/privacy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RowLayout
|
||||||
|
label={t['com.affine.mobile.setting.others.terms']()}
|
||||||
|
href="https://affine.pro/terms"
|
||||||
|
/>
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
28
packages/frontend/mobile/src/views/settings/row.layout.tsx
Normal file
28
packages/frontend/mobile/src/views/settings/row.layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { DualLinkIcon } from '@blocksuite/icons/rc';
|
||||||
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './style.css';
|
||||||
|
|
||||||
|
export const RowLayout = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
href,
|
||||||
|
}: PropsWithChildren<{ label: ReactNode; href?: string }>) => {
|
||||||
|
const content = (
|
||||||
|
<div className={styles.baseSettingItem}>
|
||||||
|
<div className={styles.baseSettingItemName}>{label}</div>
|
||||||
|
<div className={styles.baseSettingItemAction}>
|
||||||
|
{children ||
|
||||||
|
(href ? <DualLinkIcon className={styles.linkIcon} /> : null)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return href ? (
|
||||||
|
<a target="_blank" href={href} rel="noreferrer">
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
);
|
||||||
|
};
|
||||||
49
packages/frontend/mobile/src/views/settings/style.css.ts
Normal file
49
packages/frontend/mobile/src/views/settings/style.css.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const pageTitle = style({
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: '22px',
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: -0.43,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const root = style({
|
||||||
|
padding: '24px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const baseSettingItem = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 32,
|
||||||
|
padding: '8px 0',
|
||||||
|
});
|
||||||
|
export const baseSettingItemName = style({
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: '22px',
|
||||||
|
letterSpacing: -0.43,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
|
||||||
|
flexShrink: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
export const baseSettingItemAction = style([
|
||||||
|
baseSettingItemName,
|
||||||
|
{
|
||||||
|
color: cssVarV2('text/placeholder'),
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const linkIcon = style({
|
||||||
|
fontSize: 24,
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Avatar } from '@affine/component';
|
||||||
|
import { authAtom } from '@affine/core/atoms';
|
||||||
|
import { useSignOut } from '@affine/core/hooks/affine/use-sign-out';
|
||||||
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||||
|
import {
|
||||||
|
useEnsureLiveData,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { UserPlanTag } from '../../../components';
|
||||||
|
import { SettingGroup } from '../group';
|
||||||
|
import * as styles from './style.css';
|
||||||
|
|
||||||
|
export const UserProfile = () => {
|
||||||
|
const session = useService(AuthService).session;
|
||||||
|
const loginStatus = useLiveData(session.status$);
|
||||||
|
|
||||||
|
return loginStatus === 'authenticated' ? (
|
||||||
|
<AuthorizedUserProfile />
|
||||||
|
) : (
|
||||||
|
<UnauthorizedUserProfile />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BaseLayout = ({
|
||||||
|
avatar,
|
||||||
|
title,
|
||||||
|
caption,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
avatar: ReactNode;
|
||||||
|
title: ReactNode;
|
||||||
|
caption: ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<SettingGroup contentStyle={{ padding: '10px 8px 10px 10px' }}>
|
||||||
|
<div className={styles.profile} onClick={onClick}>
|
||||||
|
<div className={styles.avatarWrapper}>{avatar}</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
<div className={styles.caption}>{caption}</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRightSmallIcon className={styles.suffixIcon} />
|
||||||
|
</div>
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthorizedUserProfile = () => {
|
||||||
|
const session = useService(AuthService).session;
|
||||||
|
const account = useEnsureLiveData(session.account$);
|
||||||
|
const confirmSignOut = useSignOut();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseLayout
|
||||||
|
avatar={
|
||||||
|
<Avatar
|
||||||
|
size={48}
|
||||||
|
rounded={4}
|
||||||
|
url={account.avatar}
|
||||||
|
name={account.label}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
caption={<span className={styles.emailInfo}>{account.email}</span>}
|
||||||
|
title={
|
||||||
|
<div className={styles.nameWithTag}>
|
||||||
|
<span className={styles.name}>{account.label}</span>
|
||||||
|
<UserPlanTag />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onClick={confirmSignOut}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnauthorizedUserProfile = () => {
|
||||||
|
const setAuthModal = useSetAtom(authAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseLayout
|
||||||
|
onClick={() => setAuthModal({ openModal: true, state: 'signIn' })}
|
||||||
|
avatar={<Avatar size={48} rounded={4} />}
|
||||||
|
title="Sign up / Sign in"
|
||||||
|
caption="Sync with AFFiNE Cloud"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const profile = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 13,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const avatarWrapper = style({
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const content = style({
|
||||||
|
width: 0,
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 6,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ellipsis = style({
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: '22px',
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: -0.43,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const caption = style({
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: -0.23,
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const suffixIcon = style({
|
||||||
|
fontSize: 30,
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const emailInfo = style([ellipsis, { width: '100%' }]);
|
||||||
|
export const nameWithTag = style({
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
export const name = style([ellipsis]);
|
||||||
138
packages/frontend/mobile/src/views/settings/user-usage/index.tsx
Normal file
138
packages/frontend/mobile/src/views/settings/user-usage/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
AuthService,
|
||||||
|
ServerConfigService,
|
||||||
|
UserCopilotQuotaService,
|
||||||
|
UserQuotaService,
|
||||||
|
} from '@affine/core/modules/cloud';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { SettingGroup } from '../group';
|
||||||
|
import * as styles from './style.css';
|
||||||
|
|
||||||
|
export const UserUsage = () => {
|
||||||
|
const session = useService(AuthService).session;
|
||||||
|
const loginStatus = useLiveData(session.status$);
|
||||||
|
|
||||||
|
if (loginStatus !== 'authenticated') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <UsagePanel />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Progress = ({
|
||||||
|
name,
|
||||||
|
percent,
|
||||||
|
desc,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
percent: number;
|
||||||
|
desc: string;
|
||||||
|
color: string | null;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.progressRoot}>
|
||||||
|
<div className={styles.progressInfoRow}>
|
||||||
|
<span className={styles.progressName}>{name}</span>
|
||||||
|
<span className={styles.progressDesc}>{desc}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.progressTrack}>
|
||||||
|
<div
|
||||||
|
className={styles.progressBar}
|
||||||
|
style={{
|
||||||
|
width: `${percent}%`,
|
||||||
|
backgroundColor: color ?? cssVarV2('button/primary'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsagePanel = () => {
|
||||||
|
const serverConfigService = useService(ServerConfigService);
|
||||||
|
const serverFeatures = useLiveData(
|
||||||
|
serverConfigService.serverConfig.features$
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingGroup title="Storage">
|
||||||
|
<CloudUsage />
|
||||||
|
{serverFeatures?.copilot ? <AiUsage /> : null}
|
||||||
|
</SettingGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CloudUsage = () => {
|
||||||
|
const quota = useService(UserQuotaService).quota;
|
||||||
|
|
||||||
|
const color = useLiveData(quota.color$);
|
||||||
|
const usedFormatted = useLiveData(quota.usedFormatted$);
|
||||||
|
const maxFormatted = useLiveData(quota.maxFormatted$);
|
||||||
|
const percent = useLiveData(quota.percent$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// revalidate quota to get the latest status
|
||||||
|
quota.revalidate();
|
||||||
|
}, [quota]);
|
||||||
|
|
||||||
|
const loading = percent === null;
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Progress
|
||||||
|
name="Cloud"
|
||||||
|
percent={percent}
|
||||||
|
desc={`${usedFormatted}/${maxFormatted}`}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const AiUsage = () => {
|
||||||
|
const copilotQuotaService = useService(UserCopilotQuotaService);
|
||||||
|
|
||||||
|
const copilotActionLimit = useLiveData(
|
||||||
|
copilotQuotaService.copilotQuota.copilotActionLimit$
|
||||||
|
);
|
||||||
|
const copilotActionUsed = useLiveData(
|
||||||
|
copilotQuotaService.copilotQuota.copilotActionUsed$
|
||||||
|
);
|
||||||
|
const loading = copilotActionLimit === null || copilotActionUsed === null;
|
||||||
|
const loadError = useLiveData(copilotQuotaService.copilotQuota.error$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
copilotQuotaService.copilotQuota.revalidate();
|
||||||
|
}, [copilotQuotaService]);
|
||||||
|
|
||||||
|
if (loading || loadError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copilotActionLimit === 'unlimited') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percent = Math.min(
|
||||||
|
100,
|
||||||
|
Math.max(
|
||||||
|
0.5,
|
||||||
|
Number(((copilotActionUsed / copilotActionLimit) * 100).toFixed(4))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Progress
|
||||||
|
name="AI"
|
||||||
|
percent={percent}
|
||||||
|
desc={`${copilotActionUsed}/${copilotActionLimit}`}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const progressRoot = style({
|
||||||
|
paddingBottom: 8,
|
||||||
|
});
|
||||||
|
export const progressInfoRow = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingBottom: 4,
|
||||||
|
});
|
||||||
|
export const progressName = style({
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: '22px',
|
||||||
|
letterSpacing: -0.43,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
});
|
||||||
|
export const progressDesc = style({
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '16px',
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
});
|
||||||
|
export const progressTrack = style({
|
||||||
|
width: '100%',
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
export const progressBar = style({
|
||||||
|
height: 'inherit',
|
||||||
|
borderTopRightRadius: 5,
|
||||||
|
borderBottomRightRadius: 5,
|
||||||
|
});
|
||||||
19
yarn.lock
19
yarn.lock
@@ -683,7 +683,12 @@ __metadata:
|
|||||||
cross-env: "npm:^7.0.3"
|
cross-env: "npm:^7.0.3"
|
||||||
figma-squircle: "npm:^0.3.1"
|
figma-squircle: "npm:^0.3.1"
|
||||||
intl-segmenter-polyfill-rs: "npm:^0.1.7"
|
intl-segmenter-polyfill-rs: "npm:^0.1.7"
|
||||||
|
jotai: "npm:^2.9.3"
|
||||||
|
jotai-devtools: "npm:^0.10.1"
|
||||||
|
jotai-effect: "npm:^1.0.2"
|
||||||
|
jotai-scope: "npm:^0.7.2"
|
||||||
lodash-es: "npm:^4.17.21"
|
lodash-es: "npm:^4.17.21"
|
||||||
|
next-themes: "npm:^0.3.0"
|
||||||
react: "npm:^18.2.0"
|
react: "npm:^18.2.0"
|
||||||
react-dom: "npm:^18.2.0"
|
react-dom: "npm:^18.2.0"
|
||||||
react-router-dom: "npm:^6.26.1"
|
react-router-dom: "npm:^6.26.1"
|
||||||
@@ -25076,7 +25081,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jotai-devtools@npm:^0.10.0":
|
"jotai-devtools@npm:^0.10.0, jotai-devtools@npm:^0.10.1":
|
||||||
version: 0.10.1
|
version: 0.10.1
|
||||||
resolution: "jotai-devtools@npm:0.10.1"
|
resolution: "jotai-devtools@npm:0.10.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -25097,16 +25102,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jotai-effect@npm:^1.0.0":
|
"jotai-effect@npm:^1.0.0, jotai-effect@npm:^1.0.2":
|
||||||
version: 1.0.0
|
version: 1.0.2
|
||||||
resolution: "jotai-effect@npm:1.0.0"
|
resolution: "jotai-effect@npm:1.0.2"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
jotai: ">=2.5.0"
|
jotai: ">=2.5.0"
|
||||||
checksum: 10/4393c88deaebbfd4e8fad46ac1b7d03235d0dd684c56049e8c57b14a1298695e80b047c0b1561cbb5af694db9ea819aaaf6ae9c1a35b27d4ef98532b39065d15
|
checksum: 10/8435562902ff633138cb45ca987837c291dd4ab56d408affa5e14e167c4ddf33dd0d70dcffaef0520e38e84334583221b262fce4d45cc8ece3a195144e7a58e6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jotai-scope@npm:^0.7.0":
|
"jotai-scope@npm:^0.7.0, jotai-scope@npm:^0.7.2":
|
||||||
version: 0.7.2
|
version: 0.7.2
|
||||||
resolution: "jotai-scope@npm:0.7.2"
|
resolution: "jotai-scope@npm:0.7.2"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -25116,7 +25121,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jotai@npm:^2.8.0":
|
"jotai@npm:^2.8.0, jotai@npm:^2.9.3":
|
||||||
version: 2.9.3
|
version: 2.9.3
|
||||||
resolution: "jotai@npm:2.9.3"
|
resolution: "jotai@npm:2.9.3"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user