From 77ba1158f75afee365f0c6e267f4e0256c4dd919 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Fri, 18 Sep 2026 20:07:02 +0800 Subject: [PATCH 1/2] fix(mobile-web): keep settings titles on the sheet centerline The settings header used leftover-space flex alignment plus asymmetric padding, so the title sat left of the handle. Use a balanced three-column grid on MobileSheet and the settings override, and keep left-aligned composer and file-editor headers on flex. --- .../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 + .../src/styles/components/harmony-native.scss | 17 +++++++++-- .../tests/mobile-ui-components.test.mjs | 28 +++++++++++++++++++ 7 files changed, 70 insertions(+), 9 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/src/styles/components/harmony-native.scss b/src/mobile-web/src/styles/components/harmony-native.scss index efb04dedb2..6cb936bcac 100644 --- a/src/mobile-web/src/styles/components/harmony-native.scss +++ b/src/mobile-web/src/styles/components/harmony-native.scss @@ -659,19 +659,31 @@ } .harmony-sidebar__settings-sheet > [data-openbitfun-part='header'] { - display: flex; + display: grid; align-items: center; + grid-template-columns: var(--openbitfun-space-12) minmax(0, 1fr) var(--openbitfun-space-12); flex: 0 0 56px; height: 56px; - padding: 0 12px 0 20px; + padding: 0 var(--openbitfun-space-3); border-bottom: 1px solid var(--harmony-line); } +.harmony-sidebar__settings-sheet > [data-openbitfun-part='header'] [data-openbitfun-part='heading'] { + grid-column: 2; + text-align: center; +} + +.harmony-sidebar__settings-sheet > [data-openbitfun-part='header'] [data-openbitfun-part='header-action'] { + grid-column: 3; + justify-self: end; +} + .harmony-sidebar__settings-sheet h2 { margin: 0; color: var(--openbitfun-color-content-primary); font-size: var(--openbitfun-type-flow-section-title-font-size); font-weight: var(--openbitfun-type-heading-page-font-weight); + text-align: center; } .harmony-sidebar__settings-sheet > [data-openbitfun-part='header'] [data-openbitfun-component='mobile-icon-button'] { @@ -679,7 +691,6 @@ place-items: center; width: var(--openbitfun-space-12); height: var(--openbitfun-space-12); - margin-left: auto; padding: 0; border: 0; border-radius: 18px; diff --git a/src/mobile-web/tests/mobile-ui-components.test.mjs b/src/mobile-web/tests/mobile-ui-components.test.mjs index eea3305905..586c3c2eeb 100644 --- a/src/mobile-web/tests/mobile-ui-components.test.mjs +++ b/src/mobile-web/tests/mobile-ui-components.test.mjs @@ -80,6 +80,34 @@ test('app reset stays below shared component padding and focus styles', async () assert.doesNotMatch(adaptive, /^input:focus-visible,/m, 'generic focus styles must not override text-field focus ownership'); }); +test('settings sheet title stays on the sheet centerline beside the close action', async () => { + const harmony = await readFile(path.join(sourceDirectory, 'styles/components/harmony-native.scss'), 'utf8'); + 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 headerRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\{[^}]+\}/)?.[0]; + const headingRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\[data-openbitfun-part='heading'\]\s*\{[^}]+\}/)?.[0]; + const actionRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\[data-openbitfun-part='header-action'\]\s*\{[^}]+\}/)?.[0]; + const titleRule = harmony.match(/\.harmony-sidebar__settings-sheet\s+h2\s*\{[^}]+\}/)?.[0]; + const closeRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\[data-openbitfun-component='mobile-icon-button'\]\s*\{[^}]+\}/)?.[0]; + 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.ok(headerRule, 'missing settings sheet header rule'); + assert.match(headerRule, /display:\s*grid/); + assert.match(headerRule, /grid-template-columns:\s*var\(--openbitfun-space-12\)\s+minmax\(0,\s*1fr\)\s+var\(--openbitfun-space-12\)/); + assert.doesNotMatch(headerRule, /padding:\s*0\s+12px\s+0\s+20px/); + assert.match(headerRule, /padding:\s*0\s+var\(--openbitfun-space-3\)/); + assert.match(headingRule ?? "", /grid-column:\s*2/); + assert.match(headingRule ?? "", /text-align:\s*center/); + assert.match(actionRule ?? "", /grid-column:\s*3/); + assert.match(actionRule ?? "", /justify-self:\s*end/); + assert.match(titleRule ?? "", /text-align:\s*center/); + assert.doesNotMatch(closeRule ?? "", /margin-left:\s*auto/); + 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'); From dfde8bb6b884d5027b06ee21ede2ca73c3296b6d Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Fri, 18 Sep 2026 20:10:33 +0800 Subject: [PATCH 2/2] fix(mobile-web): keep the settings title change in the settings sheet Revert the shared MobileSheet, composer, and file-editor header edits. The off-center title is owned by the settings sheet override. --- .../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 | 7 ------- 6 files changed, 6 insertions(+), 35 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 1995f511a9..5e1beb148c 100644 --- a/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.meta.ts +++ b/design-system/packages/ui/src/mobile/MobileSheet/MobileSheet.meta.ts @@ -26,6 +26,5 @@ 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 76da3c8fe8..0771c7123c 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: grid; - align-items: center; - grid-template-columns: minmax(var(--openbitfun-space-12), 1fr) minmax(0, auto) minmax(var(--openbitfun-space-12), 1fr); + display: flex; + align-items: flex-start; + justify-content: space-between; 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; grid-column: 2; } - .headerAction { grid-column: 3; justify-self: end; } + .heading { min-inline-size: 0; flex: 1; } + .headerAction { flex: 0 0 auto; margin-block-start: calc(var(--openbitfun-space-1) * -1); } .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 c5515a71a5..a2772c35f8 100644 --- a/design-system/packages/ui/tests/mobile.test.mjs +++ b/design-system/packages/ui/tests/mobile.test.mjs @@ -143,26 +143,6 @@ 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 9b570bee19..202200226c 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"] { 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="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="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 ebe036b99f..5be27a9879 100644 --- a/src/mobile-web/src/styles/components/chat-input.scss +++ b/src/mobile-web/src/styles/components/chat-input.scss @@ -501,7 +501,6 @@ } .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 586c3c2eeb..50ede3966a 100644 --- a/src/mobile-web/tests/mobile-ui-components.test.mjs +++ b/src/mobile-web/tests/mobile-ui-components.test.mjs @@ -82,15 +82,11 @@ test('app reset stays below shared component padding and focus styles', async () test('settings sheet title stays on the sheet centerline beside the close action', async () => { const harmony = await readFile(path.join(sourceDirectory, 'styles/components/harmony-native.scss'), 'utf8'); - 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 headerRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\{[^}]+\}/)?.[0]; const headingRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\[data-openbitfun-part='heading'\]\s*\{[^}]+\}/)?.[0]; const actionRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\[data-openbitfun-part='header-action'\]\s*\{[^}]+\}/)?.[0]; const titleRule = harmony.match(/\.harmony-sidebar__settings-sheet\s+h2\s*\{[^}]+\}/)?.[0]; const closeRule = harmony.match(/\.harmony-sidebar__settings-sheet\s*>\s*\[data-openbitfun-part='header'\]\s*\[data-openbitfun-component='mobile-icon-button'\]\s*\{[^}]+\}/)?.[0]; - 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.ok(headerRule, 'missing settings sheet header rule'); assert.match(headerRule, /display:\s*grid/); @@ -103,9 +99,6 @@ test('settings sheet title stays on the sheet centerline beside the close action assert.match(actionRule ?? "", /justify-self:\s*end/); assert.match(titleRule ?? "", /text-align:\s*center/); assert.doesNotMatch(closeRule ?? "", /margin-left:\s*auto/); - 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 () => {