feat(core): rewrite page-mode-switch with RadioGroup, bind hotkey with cmdk (#7758)

close AF-1170

- bump `@toeverything/theme`
- refactor page-mode-switch
  - use global `<RadioGroup />`
  - reuse for doc history
  - remove `styled` usage
  - bind hotkey via cmdk
- Update `<RadioGroup />` color scheme with latest design system
- Update right sidebar header tab style
- Update tooltip with shortcut for app nav button
This commit is contained in:
CatsJuice 2024-08-12 03:56:56 +00:00
parent 4ac9bd7790
commit 75e02bb088
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
18 changed files with 210 additions and 302 deletions

View File

@ -47,7 +47,7 @@
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@toeverything/theme": "^1.0.2",
"@toeverything/theme": "^1.0.4",
"@vanilla-extract/dynamic": "^2.1.0",
"bytes": "^3.1.2",
"check-password-strength": "^2.0.10",

View File

@ -1,7 +1,14 @@
import * as RadixRadioGroup from '@radix-ui/react-radio-group';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { createRef, memo, useCallback, useMemo, useRef } from 'react';
import {
createRef,
memo,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { withUnit } from '../../utils/with-unit';
import * as styles from './styles.css';
@ -71,6 +78,7 @@ export const RadioGroup = memo(function RadioGroup({
animationEasing = 'cubic-bezier(.18,.22,0,1)',
activeItemClassName,
activeItemStyle,
iconMode,
onChange,
}: RadioProps) {
const animationTImerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -128,23 +136,25 @@ export const RadioGroup = memo(function RadioGroup({
[animationDuration, animationEasing, finalItems]
);
const onValueChange = useCallback(
(newValue: string) => {
const oldValue = value;
if (oldValue !== newValue) {
onChange(newValue);
animate(oldValue, newValue);
}
},
[animate, onChange, value]
);
// animate on value change
// useEffect: in case that value is changed from outside
const prevValue = useRef(value);
useEffect(() => {
const currentValue = value;
const previousValue = prevValue.current;
if (currentValue !== previousValue) {
animate(previousValue, currentValue);
prevValue.current = currentValue;
}
}, [animate, value]);
return (
<RadixRadioGroup.Root
value={value}
onValueChange={onValueChange}
onValueChange={onChange}
className={styles.radioButtonGroup}
style={finalStyle}
data-icon-mode={iconMode}
>
{finalItems.map(({ customRender, ...item }, index) => {
const testId = item.testId ? { 'data-testid': item.testId } : {};
@ -173,9 +183,7 @@ export const RadioGroup = memo(function RadioGroup({
ref={item.indicatorRef}
/>
<span className={styles.radioButtonContent}>
{customRender
? customRender(item, index)
: (item.label ?? item.value)}
{customRender?.(item, index) ?? item.label ?? item.value}
</span>
</RadixRadioGroup.Item>
);

View File

@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { createVar, globalStyle, style } from '@vanilla-extract/css';
export const outerPadding = createVar('radio-outer-padding');
@ -16,16 +17,22 @@ export const radioButton = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: cssVar('textSecondaryColor'),
color: cssVarV2('switch/fontColor/tertiary'),
whiteSpace: 'nowrap',
userSelect: 'none',
fontWeight: 600,
selectors: {
'&[data-state="checked"]': {
color: cssVar('textPrimaryColor'),
color: cssVarV2('switch/fontColor/primary'),
},
'&[data-state="unchecked"]:hover': {
background: cssVar('hoverColor'),
background: cssVarV2('switch/buttonBackground/hover'),
},
'[data-icon-mode=true] &': {
color: cssVarV2('switch/iconColor/default'),
},
'[data-icon-mode=true] &[data-state="checked"]': {
color: cssVarV2('switch/iconColor/active'),
},
},
});
@ -37,7 +44,7 @@ globalStyle(`${radioButtonContent} > svg`, { display: 'block' });
export const radioButtonGroup = style({
display: 'inline-flex',
alignItems: 'center',
background: cssVar('hoverColorFilled'),
background: cssVarV2('switch/switchBackground/background'),
borderRadius: outerRadius,
padding: outerPadding,
@ -53,8 +60,8 @@ export const indicator = style({
height: '100%',
left: 0,
top: 0,
background: cssVar('white'),
filter: 'drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.1))',
background: cssVarV2('switch/buttonBackground/active'),
boxShadow: cssVar('buttonShadow'),
opacity: 0,
transformOrigin: 'left',
selectors: {

View File

@ -47,6 +47,10 @@ export interface RadioProps extends RadioGroupItemProps {
activeItemClassName?: string;
/** Customize active item's style */
activeItemStyle?: CSSProperties;
/**
* This prop is used to use a different color scheme
*/
iconMode?: boolean;
}
export interface RadioItem {

View File

@ -48,7 +48,7 @@
"@sentry/integrations": "^7.109.0",
"@sentry/react": "^8.0.0",
"@sgtpooki/file-type": "^1.0.1",
"@toeverything/theme": "^1.0.2",
"@toeverything/theme": "^1.0.4",
"@vanilla-extract/dynamic": "^2.1.0",
"animejs": "^3.2.2",
"async-call-rpc": "^6.4.2",

View File

@ -34,11 +34,7 @@ import { encodeStateAsUpdate } from 'yjs';
import { pageHistoryModalAtom } from '../../../atoms/page-history';
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
import {
EdgelessSwitchItem,
PageSwitchItem,
} from '../../blocksuite/block-suite-mode-switch/switch-items';
import { PureEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch';
import { AffineErrorBoundary } from '../affine-error-boundary';
import {
historyListGroupByDay,
@ -108,15 +104,11 @@ const HistoryEditorPreview = ({
mode,
title,
}: HistoryEditorPreviewProps) => {
const { onSwitchToPageMode, onSwitchToEdgelessMode } = useMemo(
() => ({
onSwitchToPageMode: () => {
onModeChange('page');
},
onSwitchToEdgelessMode: () => {
onModeChange('edgeless');
},
}),
const onModeChangeWithTrack = useCallback(
(mode: DocMode) => {
track.$.docHistory.$.switchPageMode({ mode });
onModeChange(mode);
},
[onModeChange]
);
@ -124,22 +116,7 @@ const HistoryEditorPreview = ({
return (
<div className={styles.previewContent}>
<div className={styles.previewHeader}>
<StyledEditorModeSwitch switchLeft={mode === 'page'}>
<PageSwitchItem
data-testid="switch-page-mode-button"
active={mode === 'page'}
data-event-props="$.docHistory.$.switchPageMode"
data-event-args-type="page"
onClick={onSwitchToPageMode}
/>
<EdgelessSwitchItem
data-testid="switch-edgeless-mode-button"
active={mode === 'edgeless'}
data-event-props="$.docHistory.$.switchPageMode"
data-event-args-type="edgeless"
onClick={onSwitchToEdgelessMode}
/>
</StyledEditorModeSwitch>
<PureEditorModeSwitch mode={mode} setMode={onModeChangeWithTrack} />
<div className={styles.previewHeaderTitle}>{title}</div>
<div className={styles.previewHeaderTimestamp}>
{ts
@ -170,14 +147,7 @@ const HistoryEditorPreview = ({
)}
</div>
);
}, [
mode,
onSwitchToEdgelessMode,
onSwitchToPageMode,
snapshotPage,
title,
ts,
]);
}, [mode, onModeChangeWithTrack, snapshotPage, title, ts]);
return (
<div className={styles.previewWrapper}>

View File

@ -1,87 +1,69 @@
import { Tooltip } from '@affine/component/ui/tooltip';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { RadioGroup, type RadioItem, toast, Tooltip } from '@affine/component';
import { registerAffineCommand } from '@affine/core/commands';
import { track } from '@affine/core/mixpanel';
import { useI18n } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
import {
type DocMode,
DocService,
DocsService,
useLiveData,
useService,
} from '@toeverything/infra';
import type { CSSProperties } from 'react';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import type { DocCollection } from '../../../shared';
import { toast } from '../../../utils';
import { StyledEditorModeSwitch } from './style';
import { switchItem } from './style.css';
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
export type EditorModeSwitchProps = {
// todo(himself65): combine these two properties
docCollection: DocCollection;
export interface EditorModeSwitchProps {
pageId: string;
style?: CSSProperties;
isPublic?: boolean;
publicMode?: DocMode;
}
const EdgelessRadioItem: RadioItem = {
value: 'edgeless',
label: <EdgelessSwitchItem />,
testId: 'switch-edgeless-mode-button',
className: switchItem,
};
const PageRadioItem: RadioItem = {
value: 'page',
label: <PageSwitchItem />,
testId: 'switch-page-mode-button',
className: switchItem,
};
export const EditorModeSwitch = ({
style,
docCollection,
pageId,
isPublic,
publicMode,
}: EditorModeSwitchProps) => {
const t = useI18n();
const pageMeta = useBlockSuiteDocMeta(docCollection).find(
meta => meta.id === pageId
);
const trash = pageMeta?.trash ?? false;
const doc = useService(DocService).doc;
const docsService = useService(DocsService);
const doc = useLiveData(docsService.list.doc$(pageId));
const trash = useLiveData(doc?.trash$);
const currentMode = useLiveData(doc?.mode$);
const currentMode = useLiveData(doc.mode$);
useEffect(() => {
if (trash || isPublic) {
return;
}
const keydown = (e: KeyboardEvent) => {
if (e.code === 'KeyS' && e.altKey) {
e.preventDefault();
doc.toggleMode();
toast(
currentMode === 'page'
? t['com.affine.toastMessage.edgelessMode']()
: t['com.affine.toastMessage.pageMode']()
);
}
};
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [currentMode, isPublic, doc, pageId, t, trash]);
const onSwitchToPageMode = useCallback(() => {
track.$.header.actions.switchPageMode({
mode: 'page',
});
if (currentMode === 'page' || isPublic) {
return;
}
doc.setMode('page');
const togglePage = useCallback(() => {
if (currentMode === 'page' || isPublic || trash) return;
doc?.setMode('page');
toast(t['com.affine.toastMessage.pageMode']());
}, [currentMode, isPublic, doc, t]);
track.$.header.actions.switchPageMode({ mode: 'page' });
}, [currentMode, doc, isPublic, t, trash]);
const onSwitchToEdgelessMode = useCallback(() => {
track.$.header.actions.switchPageMode({
mode: 'edgeless',
});
if (currentMode === 'edgeless' || isPublic) {
return;
}
doc.setMode('edgeless');
const toggleEdgeless = useCallback(() => {
if (currentMode === 'edgeless' || isPublic || trash) return;
doc?.setMode('edgeless');
toast(t['com.affine.toastMessage.edgelessMode']());
}, [currentMode, isPublic, doc, t]);
track.$.header.actions.switchPageMode({ mode: 'edgeless' });
}, [currentMode, doc, isPublic, t, trash]);
const onModeChange = useCallback(
(mode: DocMode) => {
mode === 'page' ? togglePage() : toggleEdgeless();
},
[toggleEdgeless, togglePage]
);
const shouldHide = useCallback(
(mode: DocMode) =>
@ -89,40 +71,73 @@ export const EditorModeSwitch = ({
[currentMode, isPublic, publicMode, trash]
);
const shouldActive = useCallback(
(mode: DocMode) => (isPublic ? false : currentMode === mode),
[currentMode, isPublic]
);
useEffect(() => {
if (trash || isPublic || currentMode === undefined) return;
return registerAffineCommand({
id: 'affine:doc-mode-switch',
category: 'editor:page',
label:
currentMode === 'page'
? t['com.affine.cmdk.switch-to-edgeless']()
: t['com.affine.cmdk.switch-to-page'](),
icon: currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />,
keyBinding: {
binding: 'Alt+KeyS',
capture: true,
},
run: () => onModeChange(currentMode === 'edgeless' ? 'page' : 'edgeless'),
});
}, [currentMode, isPublic, onModeChange, t, trash]);
return (
<Tooltip
content={t['Switch']()}
shortcut={['$alt', 'S']}
side="bottom"
options={{
hidden: isPublic || trash,
}}
options={{ hidden: isPublic || trash }}
>
<StyledEditorModeSwitch
style={style}
switchLeft={currentMode === 'page'}
showAlone={trash || isPublic}
>
<PageSwitchItem
data-testid="switch-page-mode-button"
active={shouldActive('page')}
hide={shouldHide('page')}
trash={trash}
onClick={onSwitchToPageMode}
<div>
<PureEditorModeSwitch
mode={currentMode}
setMode={onModeChange}
hidePage={shouldHide('page')}
hideEdgeless={shouldHide('edgeless')}
/>
<EdgelessSwitchItem
data-testid="switch-edgeless-mode-button"
active={shouldActive('edgeless')}
hide={shouldHide('edgeless')}
trash={trash}
onClick={onSwitchToEdgelessMode}
/>
</StyledEditorModeSwitch>
</div>
</Tooltip>
);
};
export interface PureEditorModeSwitchProps {
mode?: DocMode;
setMode: (mode: DocMode) => void;
hidePage?: boolean;
hideEdgeless?: boolean;
}
export const PureEditorModeSwitch = ({
mode,
setMode,
hidePage,
hideEdgeless,
}: PureEditorModeSwitchProps) => {
const items = useMemo(
() => [
...(hidePage ? [] : [PageRadioItem]),
...(hideEdgeless ? [] : [EdgelessRadioItem]),
],
[hideEdgeless, hidePage]
);
return (
<RadioGroup
iconMode
itemHeight={24}
borderRadius={8}
padding={4}
gap={8}
value={mode}
items={items}
onChange={setMode}
/>
);
};

View File

@ -0,0 +1,10 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const switchItem = style({
width: 24,
height: 24,
});
// a workaround to override lottie svg stroke with default radio button color schemes
globalStyle(`${switchItem} svg path`, {
stroke: 'currentColor',
});

View File

@ -1,62 +0,0 @@
import { displayFlex, styled } from '@affine/component';
// TODO(@CatsJuice): refactor this component
export const StyledEditorModeSwitch = styled('div')<{
switchLeft: boolean;
showAlone?: boolean;
}>(({ switchLeft, showAlone }) => {
return {
maxWidth: showAlone ? '40px' : '70px',
gap: '8px',
height: '32px',
background: showAlone
? 'transparent'
: 'var(--affine-background-secondary-color)',
borderRadius: '8px',
...displayFlex('space-between', 'center'),
padding: '4px 4px',
position: 'relative',
'::after': {
content: '""',
display: showAlone ? 'none' : 'block',
width: '24px',
height: '24px',
background: 'var(--affine-background-primary-color)',
boxShadow: 'var(--affine-shadow-1)',
borderRadius: '4px',
zIndex: 1,
position: 'absolute',
transform: `translateX(${switchLeft ? '0' : '32px'})`,
transition: 'all .15s',
},
};
});
export const StyledSwitchItem = styled('button')<{
active?: boolean;
hide?: boolean;
trash?: boolean;
}>(({ active = false, hide = false, trash = false }) => {
return {
width: '24px',
height: '24px',
borderRadius: '8px',
WebkitAppRegion: 'no-drag',
boxShadow: active ? 'var(--affine-shadow-1)' : 'none',
color: active
? trash
? 'var(--affine-error-color)'
: 'var(--affine-primary-color)'
: 'var(--affine-icon-color)',
display: hide ? 'none' : 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
zIndex: 2,
fontSize: '20px',
path: {
stroke: 'currentColor',
},
};
});

View File

@ -5,35 +5,23 @@ import { cloneElement, useState } from 'react';
import edgelessHover from './animation-data/edgeless-hover.json';
import pageHover from './animation-data/page-hover.json';
import { StyledSwitchItem } from './style';
type HoverAnimateControllerProps = {
active?: boolean;
hide?: boolean;
trash?: boolean;
children: React.ReactElement;
} & HTMLAttributes<HTMLButtonElement>;
} & HTMLAttributes<HTMLDivElement>;
const HoverAnimateController = ({
active,
hide,
trash,
children,
...props
}: HoverAnimateControllerProps) => {
const [startAnimate, setStartAnimate] = useState(false);
return (
<StyledSwitchItem
hide={hide}
active={active}
data-active={active}
trash={trash}
onMouseEnter={() => {
setStartAnimate(true);
}}
onMouseLeave={() => {
setStartAnimate(false);
}}
<div
onMouseEnter={() => setStartAnimate(true)}
onMouseLeave={() => setStartAnimate(false)}
{...props}
>
{cloneElement(children, {
@ -42,7 +30,7 @@ const HoverAnimateController = ({
width: 20,
height: 20,
})}
</StyledSwitchItem>
</div>
);
};

View File

@ -2,9 +2,8 @@ import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n';
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect } from 'react';
import { useGeneralShortcuts } from '../../../hooks/affine/use-shortcuts';
import { NavigatorService } from '../services/navigator';
import * as styles from './navigation-buttons.css';
@ -13,22 +12,6 @@ const tooltipSideBottom = { side: 'bottom' as const };
export const NavigationButtons = () => {
const t = useI18n();
const shortcuts = useGeneralShortcuts().shortcuts;
const shortcutsObject = useMemo(() => {
const goBack = t['com.affine.keyboardShortcuts.goBack']();
const goBackShortcut = shortcuts?.[goBack];
const goForward = t['com.affine.keyboardShortcuts.goForward']();
const goForwardShortcut = shortcuts?.[goForward];
return {
goBack,
goBackShortcut,
goForward,
goForwardShortcut,
};
}, [shortcuts, t]);
const navigator = useService(NavigatorService).navigator;
const backable = useLiveData(navigator.backable$);
@ -65,11 +48,11 @@ export const NavigationButtons = () => {
return null;
}
// TODO(@CatsJuice): tooltip with shortcut
return (
<div className={styles.container}>
<IconButton
tooltip={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
tooltip={t['Go Back']()}
tooltipShortcut={['$mod', '[']}
tooltipOptions={tooltipSideBottom}
className={styles.button}
data-testid="app-navigation-button-back"
@ -80,7 +63,8 @@ export const NavigationButtons = () => {
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
tooltip={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
tooltip={t['Go Forward']()}
tooltipShortcut={['$mod', ']']}
tooltipOptions={tooltipSideBottom}
className={styles.button}
data-testid="app-navigation-button-forward"

View File

@ -289,6 +289,12 @@ const CMDKKeyBinding = ({ keyBinding }: { keyBinding: string }) => {
if (fragment === '$mod') {
return isMacOS ? '⌘' : 'Ctrl';
}
if (fragment === 'Alt') {
return isMacOS ? '⌥' : 'Alt';
}
if (fragment.startsWith('Key')) {
return fragment.slice(3);
}
if (fragment === 'ArrowUp') {
return '↑';
}

View File

@ -1,6 +1,5 @@
import { RadioGroup } from '@affine/component';
import { useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useCallback } from 'react';
import { ViewService } from '../../services/view';
@ -35,6 +34,7 @@ export const SidebarHeaderSwitcher = () => {
return tabItems.length ? (
<RadioGroup
iconMode
borderRadius={8}
itemHeight={24}
padding={4}
@ -42,7 +42,6 @@ export const SidebarHeaderSwitcher = () => {
items={tabItems}
value={activeTab?.id}
onChange={handleActiveTabChange}
activeItemStyle={{ color: cssVar('primaryColor') }}
/>
) : null;
};

View File

@ -17,12 +17,7 @@ export function ShareHeader({
}) {
return (
<div className={styles.header}>
<EditorModeSwitch
isPublic
docCollection={docCollection}
pageId={pageId}
publicMode={publishMode}
/>
<EditorModeSwitch isPublic pageId={pageId} publicMode={publishMode} />
<BlocksuiteHeaderTitle
docCollection={docCollection}
pageId={pageId}

View File

@ -81,10 +81,7 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
<Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} />
<ViewIcon icon="journal" />
<EditorModeSwitch
docCollection={workspace.docCollection}
pageId={page?.id}
/>
<EditorModeSwitch pageId={page?.id} />
<div className={styles.journalWeekPicker}>
<JournalWeekDatePicker
docCollection={workspace.docCollection}
@ -138,10 +135,7 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
<Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} />
<ViewIcon icon={currentMode ?? 'page'} />
<EditorModeSwitch
docCollection={workspace.docCollection}
pageId={page?.id}
/>
<EditorModeSwitch pageId={page?.id} />
<BlocksuiteHeaderTitle
inputHandleRef={titleInputHandleRef}
pageId={page?.id}

View File

@ -600,6 +600,8 @@
"com.affine.cmdk.no-results": "No results found",
"com.affine.cmdk.no-results-for": "No results found for",
"com.affine.cmdk.placeholder": "Type a command or search anything...",
"com.affine.cmdk.switch-to-edgeless": "Switch to $t(com.affine.edgelessMode)",
"com.affine.cmdk.switch-to-page": "Switch to $t(com.affine.pageMode)",
"com.affine.collection-bar.action.tooltip.delete": "Delete",
"com.affine.collection-bar.action.tooltip.edit": "Edit",
"com.affine.collection-bar.action.tooltip.pin": "Pin to sidebar",

View File

@ -1,54 +1,42 @@
import { expect, type Page } from '@playwright/test';
export function locateModeSwitchButton(
page: Page,
mode: 'page' | 'edgeless',
active?: boolean
) {
// switch is implemented as RadioGroup button,
// so we can use aria-checked to determine the active state
const checkedSelector = active ? '[aria-checked="true"]' : '';
return page.locator(
`[data-testid="switch-${mode}-mode-button"]${checkedSelector}`
);
}
export async function clickEdgelessModeButton(page: Page) {
await page.getByTestId('switch-edgeless-mode-button').click({
delay: 50,
});
await expect(
page.locator(
'[data-testid="switch-edgeless-mode-button"][data-active="true"]'
)
).toBeVisible();
await locateModeSwitchButton(page, 'edgeless').click({ delay: 50 });
await ensureInEdgelessMode(page);
}
export async function clickPageModeButton(page: Page) {
await page.getByTestId('switch-page-mode-button').click({
delay: 50,
});
await expect(
page.locator('[data-testid="switch-page-mode-button"][data-active="true"]')
).toBeVisible();
await locateModeSwitchButton(page, 'page').click({ delay: 50 });
await ensureInPageMode(page);
}
export async function ensureInPageMode(page: Page) {
await expect(
page.locator('[data-testid="switch-page-mode-button"][data-active="true"]')
).toBeVisible();
await expect(locateModeSwitchButton(page, 'page', true)).toBeVisible();
}
export async function ensureInEdgelessMode(page: Page) {
await expect(
page.locator(
'[data-testid="switch-edgeless-mode-button"][data-active="true"]'
)
).toBeVisible();
await expect(locateModeSwitchButton(page, 'edgeless', true)).toBeVisible();
}
export async function getPageMode(page: Page): Promise<'page' | 'edgeless'> {
if (
await page
.locator('[data-testid="switch-page-mode-button"][data-active="true"]')
.isVisible()
) {
if (await locateModeSwitchButton(page, 'page', true).isVisible()) {
return 'page';
}
if (
await page
.locator(
'[data-testid="switch-edgeless-mode-button"][data-active="true"]'
)
.isVisible()
) {
if (await locateModeSwitchButton(page, 'edgeless', true).isVisible()) {
return 'edgeless';
}
throw new Error('Unknown mode');

View File

@ -324,7 +324,7 @@ __metadata:
"@storybook/test-runner": "npm:^0.19.0"
"@storybook/testing-library": "npm:^0.2.2"
"@testing-library/react": "npm:^16.0.0"
"@toeverything/theme": "npm:^1.0.2"
"@toeverything/theme": "npm:^1.0.4"
"@types/bytes": "npm:^3.1.4"
"@types/react": "npm:^18.2.75"
"@types/react-dnd": "npm:^3.0.2"
@ -419,7 +419,7 @@ __metadata:
"@sgtpooki/file-type": "npm:^1.0.1"
"@swc/core": "npm:^1.4.13"
"@testing-library/react": "npm:^16.0.0"
"@toeverything/theme": "npm:^1.0.2"
"@toeverything/theme": "npm:^1.0.4"
"@types/animejs": "npm:^3.1.12"
"@types/bytes": "npm:^3.1.4"
"@types/image-blob-reduce": "npm:^4.1.4"
@ -14946,10 +14946,10 @@ __metadata:
languageName: unknown
linkType: soft
"@toeverything/theme@npm:^1.0.2":
version: 1.0.3
resolution: "@toeverything/theme@npm:1.0.3"
checksum: 10/2e1df60e417717aee7525d1ec0432462f95cfaded3da6ad5d7c4b273cd97b77bd3948669f56be06b592e2c6a51cd892902b7b18ba2b71360ccc0a9daba18b315
"@toeverything/theme@npm:^1.0.2, @toeverything/theme@npm:^1.0.4":
version: 1.0.4
resolution: "@toeverything/theme@npm:1.0.4"
checksum: 10/88d7cb0c2f77e1632c9a25bb6605c8df22b8b41cd7f76435c0d64beb9f4758a56ed48fb35965588c004a97634b8097ea4ebc0ea44747ce46db0e88d9a0527d26
languageName: node
linkType: hard