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');