Skip to content

Commit 07eaec2

Browse files
fix(app-shell): phone drawer closes on navigation; 44 px top-bar targets below md (#29168)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbK4VDoUnQep5cHGSLk5j5J5)_ Fixes #29075 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:手机宽度(375/390 px)下,侧边抽屉里点任一入口后抽屉会自动关上;顶栏的按钮(侧栏开关、应用图标、搜索、收件箱、头像等)和它们打开的菜单行在 768 px 以下都放大到 44 px 触控尺寸。为放得下,640 px 以下应用内顶栏不再画产品图标和 Studio 按钮,两者仍在应用切换菜单里(「Back to home」和 Studio 一行),抽屉的「All apps」也能回首页。 - **为什么改**:Mobile 2.0 用 WebView 承载 Console(#29040),手机用户只有响应式布局。实测:点抽屉入口后新页面被仍然打开的抽屉盖住,要再点一次;顶栏控件只有 28–34 px,菜单行 32/38 px,低于 44 px。 - **风险与回滚**:低。只改 `@object-ui/app-shell` 的 7 个布局文件的 `max-md:` / `max-sm:` 类名,加一个按路由关闭手机抽屉的 effect;768 px 以上外观与行为不变(1280 px 实测尺寸与 main 相同)。回滚即 revert 本 PR。 - **先看**:① `ui/packages/app-shell/src/layout/UnifiedSidebar.tsx` 新增的 `routeKey` effect;② `ui/packages/app-shell/src/layout/AppHeader.tsx` 产品图标与 Studio 按钮的 `max-sm:hidden`;③ `ui/packages/app-shell/src/layout/AppSwitcher.tsx` 的触发器与菜单行。 - **两分钟验证**:`pnpm dev:crm`,浏览器 375 px 打开 Accounts:顶栏是 ☰ · 应用图标 · 标题 · 搜索 · 铃铛 · 头像;点 ☰ 再点 Contacts,抽屉自动关上,直接看到联系人列表。 - **需要你做的**:无,席位落地。 ## What changes The Navigation flow of the Console at phone width, audited at 375×812 and 390×844 on `examples/app-crm` and `examples/app-showcase` (card #29075; parent audit #29040). - **The phone drawer closes on navigation** (`UnifiedSidebar.tsx`). Below 768 px the sidebar is a drawer over the page. A tapped entry opened its page under a drawer that stayed open, so every navigation cost a second tap. The sidebar now closes the drawer when the route (pathname and search) changes. It is keyed on the route, not on one row's handler, so navigation, favorite and recent rows and any other control that changes the route all close it. Wider windows are unchanged: the effect acts only while the sidebar is in its mobile form. - **44 px controls below `md`.** `max-md:size-11` on the sidebar trigger (class passed by `AppHeader`), the product mark, search, the Studio buttons, the AI button and the account avatar (`AppHeader.tsx`), the inbox bell (`InboxPopover.tsx`, the trigger's class only), the theme and language buttons (`ModeToggle.tsx`, `LocaleSwitcher.tsx`) and the page header's Refresh (`PageHeader.tsx`). The app pill gets `max-md:min-h-11 max-md:min-w-11` (`AppSwitcher.tsx`). - **44 px menu rows below `md`.** The app switcher, account, theme and language menus carry `max-md:[&_[role=menuitem]]:min-h-11` on their content, so every row they hold, including rows drawn from metadata (hidden apps in the account menu), is 44 px tall. The dropdown primitive (`ui/dropdown-menu.tsx`) is not edited. - **Room in a 375 px bar.** Seven 44 px controls left about 30 px for the page title. Below 640 px an app's bar therefore draws neither the product mark (and its `/` rule) nor the Studio button. Both stay one tap away: the app switcher's menu has "Back to home" and the Studio row (the bar hands it the same target), and the drawer's "All apps" goes home. Home and the other platform surfaces keep the product mark, at 44 px; the empty trigger slot there is not drawn below 640 px. Where a host hides the switcher for a single app, Studio is not offered on a phone bar; Studio is a desktop authoring surface. - No prop, export, i18n key or metadata changes. ## Run record Production Console build served at `/_console` by `objectstack dev --fresh` for "before" (main 6b3ba82); the Vite Console on the branch source against the same backends for "after". Chromium with `isMobile` and touch, DPR 2. `touch-44` lists every visible interactive element under 44 px on either side in the named scope. The keyboard check focuses the search field and shrinks the viewport by 40 % (Android `adjustResize`); iOS `visualViewport` is not simulated. | Check | Page | crm 375 | crm 390 | showcase 375 | showcase 390 | Note | |---|---|---|---|---|---|---| | touch-44 | Home (/home) | FAIL | FAIL | FAIL | FAIL | app bar fixed; page body: separate card | | no-horizontal-scroll | app landing | pass | pass | pass | pass | | | touch-44 | app landing, app bar | FAIL → pass | FAIL → pass | FAIL → pass | FAIL → pass | | | touch-44 | app switcher menu | FAIL → pass | FAIL → pass | FAIL → pass | FAIL → pass | | | overlay-fits | app switcher menu | pass | pass | pass | pass | | | touch-44 | drawer | pass | pass | pass | pass | | | overlay-fits | drawer | pass | pass | pass | pass | | | toggle | drawer nav group | pass | pass | pass | pass | | | no-horizontal-scroll | drawer scrolled | pass | pass | pass | pass | | | back-closes-drawer-stays-on-page | Back with drawer open | FAIL | FAIL | FAIL | FAIL | card filed | | drawer-closes-and-navigates | drawer entry tap | FAIL → pass | FAIL → pass | FAIL → pass | FAIL → pass | | | back-returns | Back after a drawer navigation | pass | pass | pass | pass | | | overlay-fits | search palette | pass | pass | pass | pass | | | touch-44 | search palette | FAIL | FAIL | FAIL | FAIL | Close 16 px: dialog.tsx, seat 4 | | focused-field-visible | search, 40 % keyboard | pass | pass | pass | pass | | | touch-44 | account menu | FAIL | FAIL | FAIL | FAIL | left: two notification switches, Notifications card | | no-horizontal-scroll | object list | pass | pass | pass | pass | | | touch-44 | object list | FAIL | FAIL | FAIL | FAIL | left: list toolbar, Lists card | | no-horizontal-scroll | record page | pass | pass | pass | pass | | Other checks of the card: - **No horizontal page scroll:** pass in every state measured (home, landing, switcher open, drawer open, group toggled, drawer scrolled, after navigation, search, keyboard, account menu, list, record), both examples, both widths. - **Dialogs and drawers fit the viewport:** pass (drawer 288 px wide, switcher, palette and account menu inside the viewport). - **Virtual keyboard:** pass (search field 69 to 127 px from the top with the keyboard up). - **Long lists at CPU 4×** (production build; click to the first painted frame after the drawer mounts): drawer open on app-crm 107 to 253 ms, on app-showcase (46 entries) 294 to 576 ms; nav-group toggle 173 to 252 ms; 20 frames of drawer scroll 316 to 524 ms. Drawer open on the larger app is over a 200 ms interaction budget and is not changed here: #29141. - **Back:** Back after a drawer navigation returns to the previous page (pass). Back with the drawer open leaves the page instead of closing the drawer: #29131. On the record page the phone ‹ is a link, so Back after ‹ reopens the record; routed to the Record flow with seat 4. Screenshots before and after each fix are in the project folder `issue-29075/` (`before/`, `after/`), with the run record `run-record-before.md` and the script `audit.cjs`. ## Tests - New `UnifiedSidebar.phoneDrawerCloses-29075.test.tsx` (4 tests, real `SidebarProvider` and `@object-ui/layout`): a tapped drawer entry closes the drawer; a navigation from outside the drawer closes it too; the same route leaves it open; CONTROL, a wide window's sidebar is unchanged by a route change. - New `AppHeader.phoneTargets-29075.test.tsx` (8 tests, harness of `AppHeader.sidebarTrigger-12429.test.tsx`): the 44 px classes on the bar's controls in an app and on Home; the product mark, its rule and the Studio buttons hidden below `sm` inside an app only; CONTROL, the trigger, search and avatar are never hidden; the menus' row class and the controls drawn by `AppSwitcher`, `ModeToggle`, `LocaleSwitcher`, `InboxPopover` and `PageHeader`, read from their source. - Ablation (`scripts/ablation-replace.mjs`, restored to HEAD byte-identical by blob hash): the drawer close made a no-op (`if (isMobile && Date.now() < 0) setOpenMobile(false);`) reds 2 of 4 in the drawer test, the two navigation cases. Run from `ui/` under `scripts/pm/os-verify-lock.sh`, head 0564756: - `pnpm exec vitest run --maxWorkers=1` on the two new files: exit 0, 2 files, 12 tests. - `pnpm exec vitest run --maxWorkers=2` on the 79 existing `layout/__tests__` files for `AppHeader`, `AppSwitcher`, `UnifiedSidebar`, `PageHeader`, `ConsolePageHeader`, `InboxPopover` and `LocaleSwitcher` (including `AppHeader.homeMark-12605`, which pins the product mark's classes): exit 0, 79 files, 512 tests (run at 7a92e5d; 0564756 changes only the two new test files). - `pnpm exec eslint` on the ten changed files: no errors; the warnings are the files' existing `any` and hook warnings, none on a changed line. ## Acceptance notes - Claim 6571165367, amendments 6571821121 and 6572041449 (the file surface: the seven layout files, two tests, the root changeset). - Not fixed here: Back with the drawer open, #29131; drawer open over budget at CPU 4×, #29141; the record page ‹ and Next and the palette Close (`dialog.tsx`), seat 4's Record and Overlay cards; the list toolbar, #29074; the notification switches, #29076; the home page body's small links, a card the seat is filing. - `LocalizedSidebarTrigger.tsx` was in the amended surface and needed no edit: `AppHeader` passes the class. - The derived gate `check-sdui-manifest` is green: no component config changed. ## Verification - `node scripts/pm/dispatch-gates.mjs --ran`: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit is 0. The 30 gate commands and `pnpm --dir ui exec vitest run scripts/` (169 files passed, 2 skipped) ran at 7a92e5d; 0564756 touches only the two new test files. - `node scripts/check-changeset-no-major.mjs --base origin/main`, `node scripts/check-adr-0087-registration.mjs --base origin/main`, `node scripts/check-closing-keyword-parity.mjs` and `node scripts/check-partof-closing-keyword.mjs`, with this body as the event: exit 0 each. - `node scripts/check-sdui-manifest.mjs`: exit 0. - `pnpm exec turbo run type-check --filter=@object-ui/app-shell --concurrency=2` at 0564756: exit 0, 65 of 65 tasks. - Ablation re-run at 0564756: 2 of 4 red, restored byte-identical to HEAD (blob acf4039). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (CI runs the repo lint) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jRQob8nVK5owwvNtfr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a49d633 commit 07eaec2

10 files changed

Lines changed: 530 additions & 23 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): on a phone the navigation drawer closes when an entry opens its page, and the top bar's controls and menus are 44 px touch targets
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The Console at phone width (375 and 390 px), as a WebView host shows it:
11+
12+
- **The drawer closes on navigation.** Below 768 px the sidebar is a drawer over the page. Tapping one of its entries used to open the page under a drawer that stayed open, so every navigation cost a second tap. A route change now closes the drawer, whichever entry (navigation, favorite, recent) or other control changed the route. Wider windows are unchanged.
13+
- **44 px controls below `md`.** The top bar's sidebar trigger, product mark, app pill, search, Studio button, inbox bell, AI button and account avatar measured 28 to 34 px; they are 44 px below 768 px. So are the theme and language buttons in the account menu and the page header's Refresh button. The rows of the app switcher, account, theme and language menus are 44 px tall there too (they were 32 and 38 px).
14+
- **A roomier phone bar.** Below 640 px an app's bar no longer draws the product mark (and its `/` rule) or the Studio button, which made room for the 44 px controls and the page title. Both stay one tap away: the app switcher's menu has "Back to home" and the Studio row, and the drawer's "All apps" goes home. Home and the other platform surfaces keep the product mark.

‎ui/packages/app-shell/src/layout/AppHeader.tsx‎

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,17 @@ export function AppBarCrumbTrail({
316316
* The product mark — the way home (objectui#12605). One element for every
317317
* surface that draws it: the home page, the organizations and Studio front
318318
* doors, and inside every app after the sidebar trigger. A 32 px target
319-
* holding a 24 px tile on `--primary` with the stack glyph, or the
320-
* deployment's logo. Named 「PRODUCT home」; `title` keeps the product's name.
319+
* (44 px below `md`, a phone's touch target) holding a 24 px tile on
320+
* `--primary` with the stack glyph, or the deployment's logo. Inside an app
321+
* the bar below `sm` has no room for it beside 44 px controls, so it is not
322+
* drawn there: the app switcher's 「Back to home」 and the drawer's 「All apps」
323+
* go home (objectstack#29075). Named 「PRODUCT home」; `title` keeps the product's name.
321324
*
322325
* `homePath`, not a literal `/home` (objectui#7256): on a deployment that
323326
* DECLARES a landing this is the declared one, so the mark and the post-login
324327
* landing name the same screen.
325328
*/
326-
function ProductHomeMark({ homePath, label }: { homePath: string; label: string }) {
329+
function ProductHomeMark({ homePath, label, className }: { homePath: string; label: string; className?: string }) {
327330
const productName = getProductName();
328331
const logoUrl = getLogoUrl();
329332
return (
@@ -332,7 +335,7 @@ function ProductHomeMark({ homePath, label }: { homePath: string; label: string
332335
data-testid="app-bar-product-home"
333336
aria-label={label}
334337
title={productName}
335-
className="flex size-8 shrink-0 items-center justify-center rounded-md hover:bg-accent transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring"
338+
className={cn('flex size-8 max-md:size-11 shrink-0 items-center justify-center rounded-md hover:bg-accent transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring', className)}
336339
>
337340
<span className="flex size-6 items-center justify-center overflow-hidden rounded-md bg-primary text-primary-foreground">
338341
{logoUrl ? (
@@ -844,13 +847,14 @@ export function AppHeader({
844847
control inside an app, so it never moves when the sidebar
845848
collapses; the sidebar itself carries no trigger. */}
846849
{isApp ? (
847-
<LocalizedSidebarTrigger className="shrink-0 mr-1" aria-label={t('common.toggleSidebar', { defaultValue: 'Toggle sidebar' })} />
850+
<LocalizedSidebarTrigger className="shrink-0 mr-1 max-md:size-11 max-md:mr-0" aria-label={t('common.toggleSidebar', { defaultValue: 'Toggle sidebar' })} />
848851
) : (
849852
// objectui#12605 — the product mark sits at the same x on every
850853
// surface. Off an app there is no sidebar to toggle, so the
851854
// trigger's slot (its 28 px box and its gap) is held empty: the
852-
// mark does not jump left when the viewer goes home.
853-
<span aria-hidden data-testid="app-bar-trigger-slot" className="h-7 w-7 shrink-0 mr-1" />
855+
// mark does not jump left when the viewer goes home. Below `sm` the
856+
// mark is not drawn inside an app, so there is nothing to hold.
857+
<span aria-hidden data-testid="app-bar-trigger-slot" className="h-7 w-7 shrink-0 mr-1 max-sm:hidden" />
854858
)}
855859

856860
{/* The product mark — links to home, on every surface: the home
@@ -865,12 +869,13 @@ export function AppHeader({
865869
defaultValue: '{{product}} home',
866870
product: getProductName(),
867871
})}
872+
className={isApp ? 'max-sm:hidden' : undefined}
868873
/>
869874
{isApp && (
870875
<span
871876
aria-hidden
872877
data-testid="app-bar-home-separator"
873-
className="mx-1.5 h-[18px] w-px shrink-0 rotate-[20deg] bg-border"
878+
className="mx-1.5 h-[18px] w-px shrink-0 rotate-[20deg] bg-border max-sm:hidden"
874879
/>
875880
)}
876881

@@ -1017,7 +1022,7 @@ export function AppHeader({
10171022
<Button
10181023
variant="ghost"
10191024
size="icon"
1020-
className="min-[960px]:hidden h-8 w-8 shrink-0"
1025+
className="min-[960px]:hidden h-8 w-8 max-md:size-11 shrink-0"
10211026
data-testid="action:command-palette:open-mobile"
10221027
onClick={openCommandPalette}
10231028
aria-label={searchLabel}
@@ -1039,12 +1044,15 @@ export function AppHeader({
10391044
with no owning package and for a package that is not writable
10401045
(objectui#12615): not rendered, never disabled. An icon button
10411046
at every width, like the bell beside it (objectui#12461): the pencil alone, named
1042-
「Edit app」 by its aria-label and its tooltip. */}
1047+
「Edit app」 by its aria-label and its tooltip. Below `sm` the bar
1048+
keeps its room for 44 px controls and draws neither Studio
1049+
button; the app switcher's Studio row takes the same target
1050+
(objectstack#29075). */}
10431051
{studioEntry?.posture === 'edit' && (
10441052
<Button
10451053
variant="ghost"
10461054
size="icon"
1047-
className="h-8 w-8 shrink-0"
1055+
className="h-8 w-8 max-md:size-11 max-sm:hidden shrink-0"
10481056
asChild
10491057
data-testid="app-design-in-studio-button"
10501058
aria-label={t('topbar.editApp', { defaultValue: 'Edit app' })}
@@ -1061,7 +1069,7 @@ export function AppHeader({
10611069
<Button
10621070
variant="ghost"
10631071
size="icon"
1064-
className="h-8 w-8 shrink-0"
1072+
className="h-8 w-8 max-md:size-11 max-sm:hidden shrink-0"
10651073
asChild
10661074
data-testid="app-view-in-studio-button"
10671075
aria-label={t('topbar.openAppInStudio', { defaultValue: 'Open app in Studio' })}
@@ -1105,7 +1113,7 @@ export function AppHeader({
11051113
<Button
11061114
variant="ghost"
11071115
size="icon"
1108-
className="h-8 w-8 shrink-0"
1116+
className="h-8 w-8 max-md:size-11 shrink-0"
11091117
asChild
11101118
aria-label={t('topbar.aiAssistant', { defaultValue: 'AI Assistant' })}
11111119
>
@@ -1190,7 +1198,7 @@ export function AppHeader({
11901198
<Button
11911199
variant="ghost"
11921200
size="icon"
1193-
className="h-8 w-8 shrink-0 rounded-full"
1201+
className="h-8 w-8 max-md:size-11 shrink-0 rounded-full"
11941202
aria-label={t('user.accountMenu', { defaultValue: 'Account menu' })}
11951203
>
11961204
<Avatar className="h-7 w-7 rounded-full">
@@ -1201,7 +1209,8 @@ export function AppHeader({
12011209
</Avatar>
12021210
</Button>
12031211
</DropdownMenuTrigger>
1204-
<DropdownMenuContent align="end" className="min-w-64 rounded-lg" sideOffset={4}>
1212+
{/* 44 px rows below `md`, as the app switcher's menu (objectstack#29075). */}
1213+
<DropdownMenuContent align="end" className="min-w-64 rounded-lg max-md:[&_[role=menuitem]]:min-h-11" sideOffset={4}>
12051214
{/* User identity */}
12061215
<DropdownMenuLabel className="p-0 font-normal">
12071216
<div className="flex items-center gap-2 px-2 py-2">

‎ui/packages/app-shell/src/layout/AppSwitcher.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export function AppSwitcher({ activeAppName, onAppChange, designInStudioPath, st
142142
type="button"
143143
data-testid="app-switcher-trigger"
144144
title={appLabelText}
145-
className="flex min-w-0 items-center gap-1.5 rounded-md px-1.5 py-1 text-sm font-medium text-foreground/80 hover:bg-accent hover:text-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring max-w-[40vw] sm:max-w-none"
145+
className="flex min-w-0 items-center gap-1.5 rounded-md px-1.5 py-1 text-sm font-medium text-foreground/80 hover:bg-accent hover:text-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring max-w-[40vw] sm:max-w-none max-md:min-h-11 max-md:min-w-11 max-md:justify-center"
146146
>
147147
<AppMark app={activeApp} size="sm" />
148148
{/* Below `sm` the pill is the glyph alone; `title` names the app
@@ -151,7 +151,11 @@ export function AppSwitcher({ activeAppName, onAppChange, designInStudioPath, st
151151
<ChevronDown className="hidden sm:block h-3.5 w-3.5 text-muted-foreground shrink-0" />
152152
</button>
153153
</DropdownMenuTrigger>
154-
<DropdownMenuContent align="start" sideOffset={8} className="w-64">
154+
{/* Below `md` every row is 44 px tall, a phone's touch target
155+
(objectstack#29075); the primitive's rows are 32 px. The class rides
156+
on the menu so each row it holds takes it. The top bar's other menus
157+
spell the same class. */}
158+
<DropdownMenuContent align="start" sideOffset={8} className="w-64 max-md:[&_[role=menuitem]]:min-h-11">
155159
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal">
156160
{t('layout.appSwitcher.switchApplication', { defaultValue: 'Switch Application' })}
157161
</DropdownMenuLabel>

‎ui/packages/app-shell/src/layout/InboxPopover.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ export function InboxPopover({
380380
<Button
381381
variant="ghost"
382382
size="icon"
383-
className={`h-8 w-8 relative shrink-0 ${pulse ? 'motion-safe:animate-bounce' : ''}`}
383+
className={`h-8 w-8 max-md:size-11 relative shrink-0 ${pulse ? 'motion-safe:animate-bounce' : ''}`}
384384
aria-label={ariaLabel}
385385
title={t('sidebar.inbox', { defaultValue: 'Inbox' }) as string}
386386
>

‎ui/packages/app-shell/src/layout/LocaleSwitcher.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,13 @@ export function LocaleSwitcher() {
108108
return (
109109
<DropdownMenu>
110110
<DropdownMenuTrigger asChild>
111-
<Button variant="ghost" size="icon" className="h-8 w-8">
111+
<Button variant="ghost" size="icon" className="h-8 w-8 max-md:size-11">
112112
<Globe className="h-4 w-4" />
113113
<span className="sr-only">{t('console.localeSwitcher.label')}</span>
114114
</Button>
115115
</DropdownMenuTrigger>
116-
<DropdownMenuContent align="end" className="max-h-72 overflow-y-auto">
116+
{/* 44 px rows below `md`, as the top bar's menus (objectstack#29075). */}
117+
<DropdownMenuContent align="end" className="max-h-72 overflow-y-auto max-md:[&_[role=menuitem]]:min-h-11">
117118
{offerableLanguages.map((code) => (
118119
<DropdownMenuItem
119120
key={code}

‎ui/packages/app-shell/src/layout/ModeToggle.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ export function ModeToggle() {
1010
return (
1111
<DropdownMenu>
1212
<DropdownMenuTrigger asChild>
13-
<Button variant="ghost" size="icon" className="h-8 w-8">
13+
<Button variant="ghost" size="icon" className="h-8 w-8 max-md:size-11">
1414
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
1515
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
1616
<span className="sr-only">{t('console.theme.toggle')}</span>
1717
</Button>
1818
</DropdownMenuTrigger>
19-
<DropdownMenuContent align="end">
19+
{/* 44 px rows below `md`, as the top bar's menus (objectstack#29075). */}
20+
<DropdownMenuContent align="end" className="max-md:[&_[role=menuitem]]:min-h-11">
2021
<DropdownMenuItem onClick={() => setTheme("light")} className="gap-2">
2122
<Sun className="h-4 w-4" />
2223
{t('console.theme.light')}

‎ui/packages/app-shell/src/layout/PageHeader.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export function PageHeader({
204204
type="button"
205205
variant="ghost"
206206
size="icon"
207-
className="h-8 w-8 shrink-0"
207+
className="h-8 w-8 max-md:size-11 shrink-0"
208208
onClick={refresh.onRefresh}
209209
disabled={refresh.busy}
210210
aria-label={refresh.label}

‎ui/packages/app-shell/src/layout/UnifiedSidebar.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@ export function UnifiedSidebar({ activeAppName, railOverlay = false }: UnifiedSi
214214
};
215215
}, [isMobile, setOpenMobile]);
216216

217+
// On a phone the sidebar is a drawer over the page, so a route change closes
218+
// it: an entry tapped in the drawer opens its page uncovered, whichever row
219+
// (navigation, favorite, recent) it was (objectstack#29075). Keyed on the
220+
// route, not on any one row's handler, so a row added later closes it too.
221+
const routeKey = `${location.pathname}${location.search}`;
222+
const lastRouteKey = React.useRef(routeKey);
223+
React.useEffect(() => {
224+
if (lastRouteKey.current === routeKey) return;
225+
lastRouteKey.current = routeKey;
226+
if (isMobile) setOpenMobile(false);
227+
}, [routeKey, isMobile, setOpenMobile]);
228+
217229
const { recentItems: allRecentItems } = useRecentItems();
218230
// A Studio package entry is labelled from the package list (objectui#11863),
219231
// which this sidebar does not load, so it is left out here rather than drawn

0 commit comments

Comments
 (0)