diff --git a/packages/common/infra/src/modules/db/schema/schema.ts b/packages/common/infra/src/modules/db/schema/schema.ts
index 43b750fd24..aeb980cf01 100644
--- a/packages/common/infra/src/modules/db/schema/schema.ts
+++ b/packages/common/infra/src/modules/db/schema/schema.ts
@@ -16,6 +16,7 @@ export const AFFiNE_WORKSPACE_DB_SCHEMA = {
primaryMode: f.string().optional(),
edgelessColorTheme: f.string().optional(),
journal: f.string().optional(),
+ pageWidth: f.string().optional(),
}),
docCustomPropertyInfo: {
id: f.string().primaryKey().optional().default(nanoid),
diff --git a/packages/common/infra/src/modules/doc/constants.ts b/packages/common/infra/src/modules/doc/constants.ts
index af982db0f5..3e522a215b 100644
--- a/packages/common/infra/src/modules/doc/constants.ts
+++ b/packages/common/infra/src/modules/doc/constants.ts
@@ -45,4 +45,10 @@ export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [
show: 'always-hide',
index: 'a0000007',
},
+ {
+ id: 'pageWidth',
+ type: 'pageWidth',
+ show: 'always-hide',
+ index: 'a0000008',
+ },
] as DocCustomPropertyInfo[];
diff --git a/packages/frontend/core/src/commands/affine-settings.tsx b/packages/frontend/core/src/commands/affine-settings.tsx
index 4bc1827305..0127533956 100644
--- a/packages/frontend/core/src/commands/affine-settings.tsx
+++ b/packages/frontend/core/src/commands/affine-settings.tsx
@@ -174,10 +174,10 @@ export function registerAffineSettingsCommands({
registerAffineCommand({
id: `affine:change-full-width-layout`,
label: () =>
- `${t['com.affine.cmdk.affine.full-width-layout.to']()} ${t[
+ `${t[
settings$.value.fullWidthLayout
- ? 'com.affine.cmdk.affine.switch-state.off'
- : 'com.affine.cmdk.affine.switch-state.on'
+ ? 'com.affine.cmdk.affine.default-page-width-layout.standard'
+ : 'com.affine.cmdk.affine.default-page-width-layout.full-width'
]()}`,
category: 'affine:settings',
icon: ,
diff --git a/packages/frontend/core/src/components/doc-properties/types/constant.tsx b/packages/frontend/core/src/components/doc-properties/types/constant.tsx
index 74e1982ad1..531d8cb96f 100644
--- a/packages/frontend/core/src/components/doc-properties/types/constant.tsx
+++ b/packages/frontend/core/src/components/doc-properties/types/constant.tsx
@@ -6,6 +6,7 @@ import {
EdgelessIcon,
FileIcon,
HistoryIcon,
+ LongerIcon,
NumberIcon,
TagIcon,
TextIcon,
@@ -19,6 +20,7 @@ import { DocPrimaryModeValue } from './doc-primary-mode';
import { EdgelessThemeValue } from './edgeless-theme';
import { JournalValue } from './journal';
import { NumberValue } from './number';
+import { PageWidthValue } from './page-width';
import { TagsValue } from './tags';
import { TextValue } from './text';
import type { PropertyValueProps } from './types';
@@ -100,6 +102,12 @@ export const DocPropertyTypes = {
name: 'com.affine.page-properties.property.edgelessTheme',
description: 'com.affine.page-properties.property.edgelessTheme.tooltips',
},
+ pageWidth: {
+ icon: LongerIcon,
+ value: PageWidthValue,
+ name: 'com.affine.page-properties.property.pageWidth',
+ description: 'com.affine.page-properties.property.pageWidth.tooltips',
+ },
} as Record<
string,
{
diff --git a/packages/frontend/core/src/components/doc-properties/types/page-width.css.ts b/packages/frontend/core/src/components/doc-properties/types/page-width.css.ts
new file mode 100644
index 0000000000..f375274513
--- /dev/null
+++ b/packages/frontend/core/src/components/doc-properties/types/page-width.css.ts
@@ -0,0 +1,5 @@
+import { style } from '@vanilla-extract/css';
+export const container = style({
+ paddingTop: '3px',
+ paddingBottom: '3px',
+});
diff --git a/packages/frontend/core/src/components/doc-properties/types/page-width.tsx b/packages/frontend/core/src/components/doc-properties/types/page-width.tsx
new file mode 100644
index 0000000000..d7ee69f8f0
--- /dev/null
+++ b/packages/frontend/core/src/components/doc-properties/types/page-width.tsx
@@ -0,0 +1,61 @@
+import { PropertyValue, RadioGroup, type RadioItem } from '@affine/component';
+import { EditorSettingService } from '@affine/core/modules/editor-setting';
+import { useI18n } from '@affine/i18n';
+import { DocService, useLiveData, useService } from '@toeverything/infra';
+import { useCallback, useMemo } from 'react';
+
+import { container } from './page-width.css';
+import type { PageLayoutMode } from './types';
+
+export const PageWidthValue = () => {
+ const t = useI18n();
+ const editorSetting = useService(EditorSettingService).editorSetting;
+ const defaultPageWidth = useLiveData(editorSetting.settings$).fullWidthLayout;
+
+ const doc = useService(DocService).doc;
+ const pageWidth = useLiveData(doc.properties$.selector(p => p.pageWidth));
+
+ const radioValue =
+ pageWidth ??
+ ((defaultPageWidth ? 'fullWidth' : 'standard') as PageLayoutMode);
+
+ const radioItems = useMemo(
+ () => [
+ {
+ value: 'standard' as PageLayoutMode,
+ label:
+ t[
+ 'com.affine.settings.editorSettings.page.default-page-width.standard'
+ ](),
+ testId: 'standard-width-trigger',
+ },
+ {
+ value: 'fullWidth' as PageLayoutMode,
+ label:
+ t[
+ 'com.affine.settings.editorSettings.page.default-page-width.full-width'
+ ](),
+ testId: 'full-width-trigger',
+ },
+ ],
+ [t]
+ );
+
+ const handleChange = useCallback(
+ (value: PageLayoutMode) => {
+ doc.record.setProperty('pageWidth', value);
+ },
+ [doc]
+ );
+ return (
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/components/doc-properties/types/types.ts b/packages/frontend/core/src/components/doc-properties/types/types.ts
index 93cbd7190f..3d6a885942 100644
--- a/packages/frontend/core/src/components/doc-properties/types/types.ts
+++ b/packages/frontend/core/src/components/doc-properties/types/types.ts
@@ -5,3 +5,5 @@ export interface PropertyValueProps {
value: any;
onChange: (value: any) => void;
}
+
+export type PageLayoutMode = 'standard' | 'fullWidth';
diff --git a/packages/frontend/core/src/components/hooks/affine/use-register-blocksuite-editor-commands.tsx b/packages/frontend/core/src/components/hooks/affine/use-register-blocksuite-editor-commands.tsx
index f3dc4adda4..5c73140c53 100644
--- a/packages/frontend/core/src/components/hooks/affine/use-register-blocksuite-editor-commands.tsx
+++ b/packages/frontend/core/src/components/hooks/affine/use-register-blocksuite-editor-commands.tsx
@@ -5,6 +5,7 @@ import {
} from '@affine/core/commands';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { Editor } from '@affine/core/modules/editor';
+import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
@@ -30,6 +31,11 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
const t = useI18n();
const workspace = useService(WorkspaceService).workspace;
+ const editorSetting = useService(EditorSettingService).editorSetting;
+ const defaultPageWidth = useLiveData(editorSetting.settings$).fullWidthLayout;
+ const pageWidth = useLiveData(doc.properties$.selector(p => p.pageWidth));
+ const checked = pageWidth ? pageWidth === 'fullWidth' : defaultPageWidth;
+
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
const trash = useLiveData(doc.trash$);
@@ -159,6 +165,24 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
})
);
+ unsubs.push(
+ registerAffineCommand({
+ id: `editor:page-set-width`,
+ preconditionStrategy: () => mode === 'page',
+ category: `editor:page`,
+ icon: ,
+ label: checked
+ ? t['com.affine.cmdk.affine.current-page-width-layout.standard']()
+ : t['com.affine.cmdk.affine.current-page-width-layout.full-width'](),
+ async run() {
+ doc.record.setProperty(
+ 'pageWidth',
+ checked ? 'standard' : 'fullWidth'
+ );
+ },
+ })
+ );
+
// TODO(@Peng): should not show duplicate for journal
unsubs.push(
registerAffineCommand({
@@ -308,5 +332,8 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
docId,
doc,
openInfoModal,
+ pageWidth,
+ defaultPageWidth,
+ checked,
]);
}
diff --git a/packages/frontend/core/src/components/page-detail-editor.tsx b/packages/frontend/core/src/components/page-detail-editor.tsx
index b6a8b4487e..fec5deff4a 100644
--- a/packages/frontend/core/src/components/page-detail-editor.tsx
+++ b/packages/frontend/core/src/components/page-detail-editor.tsx
@@ -1,7 +1,7 @@
import './page-detail-editor.css';
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
-import { useLiveData, useService } from '@toeverything/infra';
+import { DocService, useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import clsx from 'clsx';
import type { CSSProperties } from 'react';
@@ -33,6 +33,9 @@ export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
const mode = useLiveData(editor.mode$);
const defaultOpenProperty = useLiveData(editor.defaultOpenProperty$);
+ const doc = useService(DocService).doc;
+ const pageWidth = useLiveData(doc.properties$.selector(p => p.pageWidth));
+
const isSharedMode = editor.isSharedMode;
const editorSetting = useService(EditorSettingService).editorSetting;
const settings = useLiveData(
@@ -42,6 +45,9 @@ export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
fullWidthLayout: s.fullWidthLayout,
}))
);
+ const fullWidthLayout = pageWidth
+ ? pageWidth === 'fullWidth'
+ : settings.fullWidthLayout;
const value = useMemo(() => {
const fontStyle = fontStyleOptions.find(
@@ -60,7 +66,7 @@ export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
return (
{
const t = useI18n();
const editorSetting = useService(EditorSettingService).editorSetting;
const settings = useLiveData(editorSetting.settings$);
+ const radioItems = useMemo(
+ () => [
+ {
+ value: 'standard' as PageLayoutMode,
+ label:
+ t[
+ 'com.affine.settings.editorSettings.page.default-page-width.standard'
+ ](),
+ testId: 'standard-width-trigger',
+ },
+ {
+ value: 'fullWidth' as PageLayoutMode,
+ label:
+ t[
+ 'com.affine.settings.editorSettings.page.default-page-width.full-width'
+ ](),
+ testId: 'full-width-trigger',
+ },
+ ],
+ [t]
+ );
+
const handleFullWidthLayoutChange = useCallback(
- (checked: boolean) => {
+ (value: PageLayoutMode) => {
+ const checked = value === 'fullWidth';
editorSetting.set('fullWidthLayout', checked);
},
[editorSetting]
@@ -35,14 +61,18 @@ export const Page = () => {
return (
-
diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json
index 45df90c856..e9ee3fbba9 100644
--- a/packages/frontend/i18n/src/i18n-completenesses.json
+++ b/packages/frontend/i18n/src/i18n-completenesses.json
@@ -7,16 +7,16 @@
"es-AR": 14,
"es-CL": 16,
"es": 14,
- "fr": 71,
+ "fr": 70,
"hi": 2,
"it": 1,
"ja": 94,
- "ko": 84,
+ "ko": 83,
"pl": 0,
"pt-BR": 91,
- "ru": 78,
+ "ru": 77,
"sv-SE": 5,
"ur": 3,
"zh-Hans": 95,
- "zh-Hant": 93
+ "zh-Hant": 92
}
\ No newline at end of file
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index df725c912b..a8d90316c5 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -333,6 +333,10 @@
"com.affine.cmdk.affine.editor.trash-footer-hint": "This doc has been moved to the trash, you can either restore or permanently delete it.",
"com.affine.cmdk.affine.font-style.to": "Change font style to",
"com.affine.cmdk.affine.full-width-layout.to": "Change full width layout to",
+ "com.affine.cmdk.affine.default-page-width-layout.standard": "Change default width for new pages in to standard",
+ "com.affine.cmdk.affine.default-page-width-layout.full-width": "Change default width for new pages in to full width",
+ "com.affine.cmdk.affine.current-page-width-layout.standard": "Change current page width to standard",
+ "com.affine.cmdk.affine.current-page-width-layout.full-width": "Change current page width to full width",
"com.affine.cmdk.affine.getting-started": "Getting started",
"com.affine.cmdk.affine.import-workspace": "Import workspace",
"com.affine.cmdk.affine.insert-link": "Insert this link to the current doc",
@@ -681,6 +685,7 @@
"com.affine.page-properties.property.createdAt": "Created",
"com.affine.page-properties.property.updatedAt": "Updated",
"com.affine.page-properties.property.edgelessTheme": "Edgeless theme",
+ "com.affine.page-properties.property.pageWidth": "Page width",
"com.affine.page-properties.property.tags.tooltips": "Add relevant identifiers or categories to the doc. Useful for organizing content, improving searchability, and grouping related docs together.",
"com.affine.page-properties.property.journal.tooltips": "Indicates that this doc is a journal entry or daily note. Facilitates easy capture of ideas, quick logging of thoughts, and ongoing personal reflection.",
"com.affine.page-properties.property.checkbox.tooltips": "Use a checkbox to indicate whether a condition is true or false. Useful for confirming options, toggling features, or tracking task states.",
@@ -698,6 +703,7 @@
"com.affine.page-properties.property.createdAt.tooltips": "Track when a doc was first created. Useful for maintaining record history, sorting by creation date, or auditing content chronologically.",
"com.affine.page-properties.property.docPrimaryMode.tooltips": "Select the doc mode from Page Mode, Edgeless Mode, or Auto. Useful for choosing the best display for your content.",
"com.affine.page-properties.property.edgelessTheme.tooltips": "Select the doc theme from Light, Dark, or System. Useful for precise control over content viewing style.",
+ "com.affine.page-properties.property.pageWidth.tooltips": "Control the width of this page to fit content display needs.",
"com.affine.propertySidebar.property-list.section": "Properties",
"com.affine.propertySidebar.add-more.section": "Add more properties",
"com.affine.page-properties.settings.title": "customize properties",
@@ -1132,6 +1138,10 @@
"com.affine.settings.editorSettings.page.display-doc-info.title": "Display doc info",
"com.affine.settings.editorSettings.page.full-width.description": "Maximise display of content within a page.",
"com.affine.settings.editorSettings.page.full-width.title": "Full width layout",
+ "com.affine.settings.editorSettings.page.default-page-width.title": "Default page width",
+ "com.affine.settings.editorSettings.page.default-page-width.description": "Set default width for new pages, individual pages can override.",
+ "com.affine.settings.editorSettings.page.default-page-width.standard": "Standard",
+ "com.affine.settings.editorSettings.page.default-page-width.full-width": "Full width",
"com.affine.settings.editorSettings.page.edgeless-default-theme.description": "Set edgeless default color scheme.",
"com.affine.settings.editorSettings.page.edgeless-default-theme.title": "Edgeless default theme",
"com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "Specified by current color mode",
diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json
index d0e4227297..f9c189612d 100644
--- a/packages/frontend/i18n/src/resources/zh-Hans.json
+++ b/packages/frontend/i18n/src/resources/zh-Hans.json
@@ -330,6 +330,10 @@
"com.affine.cmdk.affine.editor.trash-footer-hint": "该文档已移至回收站,您可以恢复或永久删除它。",
"com.affine.cmdk.affine.font-style.to": "更改字体样式为",
"com.affine.cmdk.affine.full-width-layout.to": "更改全宽布局为",
+ "com.affine.cmdk.affine.default-page-width-layout.standard": "将新页面的默认宽度更改为标准",
+ "com.affine.cmdk.affine.default-page-width-layout.full-width": "将新页面的默认宽度更改为全宽",
+ "com.affine.cmdk.affine.current-page-width-layout.standard": "将当前页面宽度更改为标准",
+ "com.affine.cmdk.affine.current-page-width-layout.full-width": "将当前页面宽度更改为全宽",
"com.affine.cmdk.affine.getting-started": "开始使用",
"com.affine.cmdk.affine.import-workspace": "导入工作区",
"com.affine.cmdk.affine.insert-link": "将这个链接插入到当前文档",
@@ -666,6 +670,7 @@
"com.affine.page-properties.property.text": "文本",
"com.affine.page-properties.property.updatedBy": "最后编辑者",
"com.affine.page-properties.property.edgelessTheme": "无界配色方案",
+ "com.affine.page-properties.property.pageWidth": "页面宽度",
"com.affine.page-properties.property.createdAt": "创建时间",
"com.affine.page-properties.property.updatedAt": "更新时间",
"com.affine.page-properties.property.tags.tooltips": "为文档添加相关标识或类别,有助于组织内容、提高搜索效率并将相关文档归类。",
@@ -685,6 +690,7 @@
"com.affine.page-properties.property.createdAt.tooltips": "跟踪文档首次创建的时间。用于维护记录历史、按创建日期排序或按时间顺序审核内容。",
"com.affine.page-properties.property.docPrimaryMode.tooltips": "选择页面模式、无边界模式或自动模式,适合根据内容选择最佳显示方式。",
"com.affine.page-properties.property.edgelessTheme.tooltips": "选择浅色、深色或系统主题,精确控制内容的查看样式。",
+ "com.affine.page-properties.property.pageWidth.tooltips": "控制此页面的宽度以适合内容显示需要。",
"com.affine.page-properties.settings.title": "自定义属性",
"com.affine.page-properties.tags.open-tags-page": "打开标签页面",
"com.affine.page-properties.tags.selector-header-title": "选择或者创建一个标签",
@@ -1094,6 +1100,10 @@
"com.affine.settings.editorSettings.page.display-doc-info.title": "显示文档信息",
"com.affine.settings.editorSettings.page.full-width.description": "文档内容的最大显示量。",
"com.affine.settings.editorSettings.page.full-width.title": "全宽布局",
+ "com.affine.settings.editorSettings.page.default-page-width.title": "默认文档页面宽度",
+ "com.affine.settings.editorSettings.page.default-page-width.description": "设置新页面的默认宽度,单独在页面中设置页面宽度属性可以覆盖默认设置。",
+ "com.affine.settings.editorSettings.page.default-page-width.standard": "标准",
+ "com.affine.settings.editorSettings.page.default-page-width.full-width": "全宽",
"com.affine.settings.editorSettings.page.edgeless-default-theme.description": "设置默认的无界配色方案。",
"com.affine.settings.editorSettings.page.edgeless-default-theme.title": "无界默认配色方案",
"com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由当前应用的配色方案指定",
diff --git a/tests/affine-local/e2e/page-properties.spec.ts b/tests/affine-local/e2e/page-properties.spec.ts
index f917819867..f3220a7daa 100644
--- a/tests/affine-local/e2e/page-properties.spec.ts
+++ b/tests/affine-local/e2e/page-properties.spec.ts
@@ -130,6 +130,7 @@ test('property table reordering', async ({ page }) => {
'Updated',
'Created by',
'Edgeless theme',
+ 'Page width',
'Number',
'Date',
'Checkbox',
@@ -174,6 +175,7 @@ test('page info show more will show all properties', async ({ page }) => {
'Updated',
'Created by',
'Edgeless theme',
+ 'Page width',
'Text',
'Number',
'Date',
diff --git a/tests/affine-local/e2e/quick-search.spec.ts b/tests/affine-local/e2e/quick-search.spec.ts
index 799e1d43bd..c506c17f5c 100644
--- a/tests/affine-local/e2e/quick-search.spec.ts
+++ b/tests/affine-local/e2e/quick-search.spec.ts
@@ -137,7 +137,7 @@ test('Create a new page without keyword', async ({ page }) => {
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
- const addNewPage = page.locator('[cmdk-item] >> text=New Page');
+ const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
await page.waitForTimeout(300);
await assertTitle(page, '');
@@ -242,7 +242,7 @@ test('Focus title after creating a new page', async ({ page }) => {
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
- const addNewPage = page.locator('[cmdk-item] >> text=New Page');
+ const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
await titleIsFocused(page);
});
@@ -275,7 +275,7 @@ test('assert the recent browse pages are on the recent list', async ({
// create second page
await openQuickSearchByShortcut(page);
- const addNewPage = page.locator('[cmdk-item] >> text=New Page');
+ const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
await waitForEditorLoad(page);
{
@@ -315,7 +315,7 @@ test('assert the recent browse pages are on the recent list', async ({
await waitForEditorLoad(page);
await openQuickSearchByShortcut(page);
{
- const addNewPage = page.locator('[cmdk-item] >> text=New Page');
+ const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
}
await waitForEditorLoad(page);
diff --git a/tests/affine-local/e2e/settings.spec.ts b/tests/affine-local/e2e/settings.spec.ts
index 008d99d68c..b381098d93 100644
--- a/tests/affine-local/e2e/settings.spec.ts
+++ b/tests/affine-local/e2e/settings.spec.ts
@@ -68,7 +68,7 @@ test('Change layout width', async ({ page }) => {
await waitForEditorLoad(page);
await openEditorSetting(page);
- await page.getByTestId('full-width-layout-trigger').click();
+ await page.getByTestId('full-width-trigger').click();
const editorWrapper = page.locator('.editor-wrapper');
const className = await editorWrapper.getAttribute('class');