Skip to content

Commit 71ab75e

Browse files
committed
feat(mobile): mobile experimental feature setting (#8922)
close AF-1802 ![CleanShot 2024-11-26 at 10.02.27.gif](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/09d24e35-a524-497d-b5aa-840bf74f128f.gif)
1 parent c95e6ec commit 71ab75e

File tree

10 files changed

+236
-12
lines changed

10 files changed

+236
-12
lines changed

packages/common/infra/src/modules/feature-flag/constant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export const AFFINE_FLAGS = {
174174
'com.affine.settings.workspace.experimental-features.enable-theme-editor.name',
175175
description:
176176
'com.affine.settings.workspace.experimental-features.enable-theme-editor.description',
177-
configurable: isCanaryBuild,
177+
configurable: isCanaryBuild && !isMobile,
178178
defaultState: isCanaryBuild,
179179
},
180180
enable_local_workspace: {

packages/frontend/component/src/ui/modal/modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export interface ModalProps extends DialogProps {
4848
/**
4949
* @default 'fadeScaleTop'
5050
*/
51-
animation?: 'fadeScaleTop' | 'none' | 'slideBottom';
51+
animation?: 'fadeScaleTop' | 'none' | 'slideBottom' | 'slideRight';
5252
/**
5353
* Whether to show the modal in full screen mode
5454
*/

packages/frontend/component/src/ui/modal/styles.css.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,23 @@ const contentHideSlideBottom = keyframes({
5151
from: { transform: 'translateY(0)' },
5252
to: { transform: 'translateY(100%)' },
5353
});
54+
const contentShowSlideRight = keyframes({
55+
from: { transform: 'translateX(100%)' },
56+
to: { transform: 'translateX(0)' },
57+
});
58+
const contentHideSlideRight = keyframes({
59+
from: { transform: 'translateX(0)' },
60+
to: { transform: 'translateX(100%)' },
61+
});
5462
const modalContentViewTransitionNameFadeScaleTop = generateIdentifier(
5563
'modal-content-fade-scale-top'
5664
);
5765
const modalContentViewTransitionNameSlideBottom = generateIdentifier(
5866
'modal-content-slide-bottom'
5967
);
68+
const modalContentViewTransitionNameSlideRight = generateIdentifier(
69+
'modal-content-slide-right'
70+
);
6071
export const modalOverlay = style({
6172
position: 'fixed',
6273
inset: 0,
@@ -105,6 +116,13 @@ export const modalContentWrapper = style({
105116
[`${vtScopeSelector(modalVTScope)} &.anim-slideBottom.vt-active`]: {
106117
viewTransitionName: modalContentViewTransitionNameSlideBottom,
107118
},
119+
'&.anim-slideRight': {
120+
animation: `${contentShowSlideRight} 0.23s ease`,
121+
animationFillMode: 'forwards',
122+
},
123+
[`${vtScopeSelector(modalVTScope)} &.anim-slideRight.vt-active`]: {
124+
viewTransitionName: modalContentViewTransitionNameSlideRight,
125+
},
108126
},
109127
});
110128
globalStyle(
@@ -121,6 +139,13 @@ globalStyle(
121139
animationFillMode: 'forwards',
122140
}
123141
);
142+
globalStyle(
143+
`::view-transition-old(${modalContentViewTransitionNameSlideRight})`,
144+
{
145+
animation: `${contentHideSlideRight} 0.23s ease`,
146+
animationFillMode: 'forwards',
147+
}
148+
);
124149

125150
export const modalContent = style({
126151
vars: {

packages/frontend/component/src/ui/switch/index.css.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import { cssVar } from '@toeverything/theme';
2-
import { style } from '@vanilla-extract/css';
2+
import { createVar, style } from '@vanilla-extract/css';
3+
4+
export const switchHeightVar = createVar('switchSize');
5+
export const switchPaddingVar = createVar('switchPadding');
6+
const switchWidthVar = createVar('switchWidth');
7+
const dotSizeVar = createVar('dotSize');
8+
39
export const labelStyle = style({
10+
vars: {
11+
[switchHeightVar]: '26px',
12+
[switchPaddingVar]: '3px',
13+
[switchWidthVar]: `calc((${switchHeightVar} - ${switchPaddingVar}) * 2)`,
14+
[dotSizeVar]: `calc(${switchHeightVar} - ${switchPaddingVar} * 2)`,
15+
},
416
display: 'flex',
517
alignItems: 'center',
618
gap: '10px',
@@ -12,8 +24,8 @@ export const inputStyle = style({
1224
});
1325
export const switchStyle = style({
1426
position: 'relative',
15-
width: '46px',
16-
height: '26px',
27+
height: switchHeightVar,
28+
width: switchWidthVar,
1729
background: cssVar('toggleDisableBackgroundColor'),
1830
borderRadius: '37px',
1931
transition: '200ms all',
@@ -22,12 +34,12 @@ export const switchStyle = style({
2234
transition: 'all .2s cubic-bezier(0.27, 0.2, 0.25, 1.51)',
2335
content: '""',
2436
position: 'absolute',
25-
width: '20px',
26-
height: '20px',
37+
width: dotSizeVar,
38+
height: dotSizeVar,
2739
borderRadius: '50%',
2840
top: '50%',
2941
background: cssVar('toggleCircleBackgroundColor'),
30-
transform: 'translate(3px, -50%)',
42+
transform: `translate(${switchPaddingVar}, -50%)`,
3143
},
3244
},
3345
});
@@ -36,7 +48,7 @@ export const switchCheckedStyle = style({
3648
selectors: {
3749
'&:before': {
3850
borderColor: cssVar('pureBlack10'),
39-
transform: 'translate(23px,-50%)',
51+
transform: `translate(calc(${switchHeightVar} - ${switchPaddingVar}), -50%)`,
4052
},
4153
},
4254
});

packages/frontend/component/src/ui/switch/switch.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// components/switch.tsx
2+
import { assignInlineVars } from '@vanilla-extract/dynamic';
23
import clsx from 'clsx';
34
import type { HTMLAttributes, ReactNode } from 'react';
4-
import { useCallback, useState } from 'react';
5+
import { useCallback, useMemo, useState } from 'react';
56

67
import * as styles from './index.css';
78

@@ -10,6 +11,14 @@ export type SwitchProps = Omit<HTMLAttributes<HTMLLabelElement>, 'onChange'> & {
1011
onChange?: (checked: boolean) => void;
1112
children?: ReactNode;
1213
disabled?: boolean;
14+
/**
15+
* The height of the switch (including the padding)
16+
*/
17+
size?: number;
18+
/**
19+
* The padding of the switch
20+
*/
21+
padding?: number;
1322
};
1423

1524
export const Switch = ({
@@ -18,8 +27,13 @@ export const Switch = ({
1827
children,
1928
className,
2029
disabled,
30+
style,
31+
size: propsSize,
32+
padding: propsPadding,
2133
...otherProps
2234
}: SwitchProps) => {
35+
const size = propsSize ?? (BUILD_CONFIG.isMobileEdition ? 24 : 26);
36+
const padding = propsPadding ?? (BUILD_CONFIG.isMobileEdition ? 2 : 3);
2337
const [checkedState, setCheckedState] = useState(checkedProp);
2438

2539
const checked = onChangeProp ? checkedProp : checkedState;
@@ -35,8 +49,23 @@ export const Switch = ({
3549
[disabled, onChangeProp]
3650
);
3751

52+
const labelStyle = useMemo(
53+
() => ({
54+
...assignInlineVars({
55+
[styles.switchHeightVar]: `${size}px`,
56+
[styles.switchPaddingVar]: `${padding}px`,
57+
}),
58+
...style,
59+
}),
60+
[size, padding, style]
61+
);
62+
3863
return (
39-
<label className={clsx(styles.labelStyle, className)} {...otherProps}>
64+
<label
65+
className={clsx(styles.labelStyle, className)}
66+
style={labelStyle}
67+
{...otherProps}
68+
>
4069
{children}
4170
<input
4271
className={clsx(styles.inputStyle)}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Modal, Scrollable, Switch } from '@affine/component';
2+
import { PageHeader } from '@affine/core/mobile/components';
3+
import { useI18n } from '@affine/i18n';
4+
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
5+
import {
6+
AFFINE_FLAGS,
7+
FeatureFlagService,
8+
type Flag,
9+
useLiveData,
10+
useService,
11+
} from '@toeverything/infra';
12+
import { useCallback, useState } from 'react';
13+
14+
import { SettingGroup } from '../group';
15+
import { RowLayout } from '../row.layout';
16+
import * as styles from './styles.css';
17+
18+
export const ExperimentalFeatureSetting = () => {
19+
const [open, setOpen] = useState(false);
20+
21+
return (
22+
<>
23+
<SettingGroup title="Experimental">
24+
<RowLayout
25+
label={'Experimental Features'}
26+
onClick={() => setOpen(true)}
27+
>
28+
<ArrowRightSmallIcon fontSize={22} />
29+
</RowLayout>
30+
</SettingGroup>
31+
<Modal
32+
animation="slideRight"
33+
open={open}
34+
onOpenChange={setOpen}
35+
fullScreen
36+
contentOptions={{ className: styles.dialog }}
37+
withoutCloseButton
38+
>
39+
<ExperimentalFeatureList onBack={() => setOpen(false)} />
40+
</Modal>
41+
</>
42+
);
43+
};
44+
45+
const ExperimentalFeatureList = ({ onBack }: { onBack: () => void }) => {
46+
const featureFlagService = useService(FeatureFlagService);
47+
48+
return (
49+
<div className={styles.root}>
50+
<PageHeader back={!!onBack} backAction={onBack} className={styles.header}>
51+
<span className={styles.dialogTitle}>Experimental Features</span>
52+
</PageHeader>
53+
<Scrollable.Root className={styles.scrollArea}>
54+
<Scrollable.Viewport>
55+
<ul className={styles.content}>
56+
{Object.keys(AFFINE_FLAGS).map(key => (
57+
<ExperimentalFeaturesItem
58+
key={key}
59+
flag={featureFlagService.flags[key as keyof AFFINE_FLAGS]}
60+
/>
61+
))}
62+
</ul>
63+
</Scrollable.Viewport>
64+
<Scrollable.Scrollbar orientation="vertical" />
65+
</Scrollable.Root>
66+
</div>
67+
);
68+
};
69+
70+
const ExperimentalFeaturesItem = ({ flag }: { flag: Flag }) => {
71+
const t = useI18n();
72+
const value = useLiveData(flag.$);
73+
74+
const onChange = useCallback(
75+
(checked: boolean) => {
76+
flag.set(checked);
77+
},
78+
[flag]
79+
);
80+
81+
if (flag.configurable === false || flag.hide) {
82+
return null;
83+
}
84+
85+
return (
86+
<li>
87+
<div className={styles.itemBlock}>
88+
{t[flag.displayName]()}
89+
<Switch checked={value} onChange={onChange} />
90+
</div>
91+
{flag.description ? (
92+
<div className={styles.itemDescription}>{t[flag.description]()}</div>
93+
) : null}
94+
</li>
95+
);
96+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
bodyEmphasized,
3+
bodyRegular,
4+
footnoteRegular,
5+
} from '@toeverything/theme/typography';
6+
import { cssVarV2 } from '@toeverything/theme/v2';
7+
import { style } from '@vanilla-extract/css';
8+
9+
export const dialog = style({
10+
padding: '0 !important',
11+
background: cssVarV2('layer/background/mobile/primary'),
12+
});
13+
export const root = style({
14+
display: 'flex',
15+
flexDirection: 'column',
16+
height: '100dvh',
17+
});
18+
export const header = style({
19+
background: `${cssVarV2('layer/background/mobile/primary')} !important`,
20+
});
21+
export const dialogTitle = style([bodyEmphasized, {}]);
22+
export const scrollArea = style({
23+
height: 0,
24+
flex: 1,
25+
});
26+
27+
export const content = style({
28+
padding: '24px 16px',
29+
display: 'flex',
30+
flexDirection: 'column',
31+
gap: 16,
32+
});
33+
34+
// item
35+
export const itemBlock = style([
36+
bodyRegular,
37+
{
38+
display: 'flex',
39+
alignItems: 'center',
40+
justifyContent: 'space-between',
41+
padding: '19px 12px',
42+
background: cssVarV2('layer/background/mobile/secondary'),
43+
borderRadius: 12,
44+
},
45+
]);
46+
export const itemDescription = style([
47+
footnoteRegular,
48+
{
49+
marginTop: 4,
50+
color: cssVarV2('text/tertiary'),
51+
},
52+
]);

packages/frontend/core/src/mobile/dialogs/setting/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useEffect } from 'react';
1010

1111
import { AboutGroup } from './about';
1212
import { AppearanceGroup } from './appearance';
13+
import { ExperimentalFeatureSetting } from './experimental';
1314
import { OthersGroup } from './others';
1415
import * as styles from './style.css';
1516
import { UserProfile } from './user-profile';
@@ -25,6 +26,7 @@ const MobileSetting = () => {
2526
<UserUsage />
2627
<AppearanceGroup />
2728
<AboutGroup />
29+
<ExperimentalFeatureSetting />
2830
<OthersGroup />
2931
</div>
3032
);

packages/frontend/core/src/mobile/dialogs/setting/row.layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ export const RowLayout = ({
88
label,
99
children,
1010
href,
11-
}: PropsWithChildren<{ label: ReactNode; href?: string }>) => {
11+
onClick,
12+
}: PropsWithChildren<{
13+
label: ReactNode;
14+
href?: string;
15+
onClick?: () => void;
16+
}>) => {
1217
const content = (
1318
<ConfigModal.Row
1419
data-testid="setting-row"
1520
className={styles.baseSettingItem}
21+
onClick={onClick}
1622
>
1723
<div className={styles.baseSettingItemName}>{label}</div>
1824
<div className={styles.baseSettingItemAction}>

packages/frontend/core/src/mobile/dialogs/setting/style.css.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const baseSettingItemAction = style([
3535
textOverflow: 'ellipsis',
3636
overflow: 'hidden',
3737
flexShrink: 1,
38+
display: 'flex',
39+
alignItems: 'center',
3840
},
3941
]);
4042

0 commit comments

Comments
 (0)