From e3401dbe4d9501389d0b4596ff64be71aef45bdc Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Fri, 18 Sep 2026 20:13:01 +0800 Subject: [PATCH] fix(ui): keep MobileSheet titles on the sheet centerline MobileSheet already declared a centered title, but flex leftover space shifted it whenever headerAction was present. Use a balanced three-column grid, and keep the composer and file-editor headers on flex so their left-aligned layouts do not inherit the new default. --- .../mobile/MobileSheet/MobileSheet.meta.ts | 1 + .../mobile/MobileSheet/MobileSheet.module.css | 10 +++++----- .../packages/ui/tests/mobile.test.mjs | 20 +++++++++++++++++++ .../src/components/WorkspaceFiles.scss | 2 +- .../src/styles/components/chat-input.scss | 1 + .../tests/mobile-ui-components.test.mjs | 11 ++++++++++ 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.meta.ts b/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.meta.ts index 5e1beb148c..1995f511a9 100644 --- a/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.meta.ts +++ b/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.meta.ts @@ -26,5 +26,6 @@ export const mobileSheetMeta = { "space.2", "space.3", "space.4", + "space.12", ], } as const satisfies ComponentMeta; diff --git a/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.module.css b/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.module.css index 0771c7123c..76da3c8fe8 100644 --- a/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.module.css +++ b/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.module.css @@ -50,17 +50,17 @@ } .header { - display: flex; - align-items: flex-start; - justify-content: space-between; + display: grid; + align-items: center; + grid-template-columns: minmax(var(--openbitfun-space-12), 1fr) minmax(0, auto) minmax(var(--openbitfun-space-12), 1fr); gap: var(--openbitfun-space-3); flex: 0 0 auto; padding: var(--openbitfun-space-3) var(--openbitfun-space-4) var(--openbitfun-space-2); text-align: center; } - .heading { min-inline-size: 0; flex: 1; } - .headerAction { flex: 0 0 auto; margin-block-start: calc(var(--openbitfun-space-1) * -1); } + .heading { min-inline-size: 0; grid-column: 2; } + .headerAction { grid-column: 3; justify-self: end; } .title { min-inline-size: 0; diff --git a/design-system/packages/ui/tests/mobile.test.mjs b/design-system/packages/ui/tests/mobile.test.mjs index a2772c35f8..c5515a71a5 100644 --- a/design-system/packages/ui/tests/mobile.test.mjs +++ b/design-system/packages/ui/tests/mobile.test.mjs @@ -143,6 +143,26 @@ test("mobile sheet keeps its closed server-rendering contract", () => { assert.equal(sheet, ""); }); +test("mobile sheet header keeps the title on the sheet centerline beside header actions", async () => { + const styles = await readFile( + new URL("../src/mobile/MobileSheet/MobileSheet.module.css", import.meta.url), + "utf8", + ); + const headerRule = styles.match(/\.header\s*\{[^}]*display:[^}]+\}/)?.[0]; + const headingRule = styles.match(/\.heading\s*\{[^}]+\}/)?.[0]; + const headerActionRule = styles.match(/\.headerAction\s*\{[^}]+\}/)?.[0]; + + assert.ok(headerRule, "missing MobileSheet header display rule"); + assert.match(headerRule, /display:\s*grid/); + assert.match( + headerRule, + /grid-template-columns:\s*minmax\([^,]+,\s*1fr\)\s+minmax\(0,\s*auto\)\s+minmax\([^,]+,\s*1fr\)/, + ); + assert.match(headingRule ?? "", /grid-column:\s*2/); + assert.match(headerActionRule ?? "", /grid-column:\s*3/); + assert.match(headerActionRule ?? "", /justify-self:\s*end/); +}); + test("mobile confirmation sheets present custom icons without a decorative tile", async () => { const styles = await readFile( new URL("../src/mobile/MobileConfirmSheet/MobileConfirmSheet.module.css", import.meta.url), diff --git a/src/mobile-web/src/components/WorkspaceFiles.scss b/src/mobile-web/src/components/WorkspaceFiles.scss index 202200226c..9b570bee19 100644 --- a/src/mobile-web/src/components/WorkspaceFiles.scss +++ b/src/mobile-web/src/components/WorkspaceFiles.scss @@ -51,7 +51,7 @@ .runtime-file-editor[data-openbitfun-part="surface"] { inline-size: 100%; max-inline-size: none; block-size: 100dvh; max-block-size: 100dvh; margin: 0; border: 0; border-radius: 0; box-shadow: none; - [data-openbitfun-part="header"] { align-items: center; padding-top: max(16px, env(safe-area-inset-top)); border-bottom: 1px solid var(--openbitfun-color-border-subtle); } + [data-openbitfun-part="header"] { display: flex; align-items: center; padding-top: max(16px, env(safe-area-inset-top)); border-bottom: 1px solid var(--openbitfun-color-border-subtle); } [data-openbitfun-part="heading"] { text-align: left; } [data-openbitfun-part="body"] { display: flex; flex: 1; min-height: 0; flex-direction: column; padding: 0; overflow: hidden; } [data-openbitfun-component="mobile-textarea"] { flex: 1; min-height: 0; border: 0; border-radius: 0; } diff --git a/src/mobile-web/src/styles/components/chat-input.scss b/src/mobile-web/src/styles/components/chat-input.scss index 5be27a9879..ebe036b99f 100644 --- a/src/mobile-web/src/styles/components/chat-input.scss +++ b/src/mobile-web/src/styles/components/chat-input.scss @@ -501,6 +501,7 @@ } .chat-composer-sheet > [data-openbitfun-part='header'] { + display: flex; align-items: center; text-align: left; } diff --git a/src/mobile-web/tests/mobile-ui-components.test.mjs b/src/mobile-web/tests/mobile-ui-components.test.mjs index 50ede3966a..32b7033363 100644 --- a/src/mobile-web/tests/mobile-ui-components.test.mjs +++ b/src/mobile-web/tests/mobile-ui-components.test.mjs @@ -101,6 +101,17 @@ test('settings sheet title stays on the sheet centerline beside the close action assert.doesNotMatch(closeRule ?? "", /margin-left:\s*auto/); }); +test('left-aligned sheet headers keep flex after MobileSheet centers by default', async () => { + const composer = await readFile(path.join(sourceDirectory, 'styles/components/chat-input.scss'), 'utf8'); + const files = await readFile(path.join(sourceDirectory, 'components/WorkspaceFiles.scss'), 'utf8'); + const composerHeader = composer.match(/\.chat-composer-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\{[^}]+\}/)?.[0]; + const editorHeader = files.match(/\[data-openbitfun-part="header"\]\s*\{[^}]+\}/)?.[0]; + + assert.match(composerHeader ?? "", /display:\s*flex/); + assert.match(composerHeader ?? "", /text-align:\s*left/); + assert.match(editorHeader ?? "", /display:\s*flex/); +}); + test('pairing and settings styles follow component parts instead of obsolete native anatomy', async () => { const harmony = await readFile(path.join(sourceDirectory, 'styles/components/harmony-native.scss'), 'utf8'); const overlays = await readFile(path.join(sourceDirectory, 'components/SessionOverlays.tsx'), 'utf8');