diff --git a/packages/app/e2e/regression/mobile-status-drawer.spec.ts b/packages/app/e2e/regression/mobile-status-drawer.spec.ts index a30d87f715c..efd6e48a1f3 100644 --- a/packages/app/e2e/regression/mobile-status-drawer.spec.ts +++ b/packages/app/e2e/regression/mobile-status-drawer.spec.ts @@ -10,7 +10,7 @@ test("status drawer dismisses and reopens after button, backdrop, Escape, and dr .locator('[data-slot="session-mobile-view-navigation"]') .getByRole("button", { name: "More options", exact: true }) const drawer = page.getByRole("dialog", { name: "Status", exact: true }) - const overlay = page.locator('[data-slot="mobile-status-overlay"]') + const overlay = page.locator('[data-slot="mobile-drawer-overlay"]') for (const dismissal of ["button", "backdrop", "escape", "drag", "button"] as const) { await more.click() @@ -21,7 +21,7 @@ test("status drawer dismisses and reopens after button, backdrop, Escape, and dr if (dismissal === "backdrop") await overlay.click({ position: { x: 10, y: 10 } }) if (dismissal === "escape") await page.keyboard.press("Escape") if (dismissal === "drag") { - const handle = drawer.locator('[data-slot="mobile-status-drag-handle"]') + const handle = drawer.locator('[data-slot="mobile-drawer-handle"]') const bounds = await handle.boundingBox() expect(bounds).not.toBeNull() await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2) diff --git a/packages/app/src/shell/mobile-drawer.css b/packages/app/src/shell/mobile-drawer.css new file mode 100644 index 00000000000..34e7577fd7d --- /dev/null +++ b/packages/app/src/shell/mobile-drawer.css @@ -0,0 +1,90 @@ +[data-slot="mobile-drawer-overlay"] { + position: fixed; + inset: 0; + z-index: 50; + background: var(--v2-overlay-simple-overlay-scrim); + animation: mobile-drawer-backdrop-in 240ms ease-out; +} + +[data-slot="mobile-drawer-overlay"]:is([data-closing], [data-closed]) { + animation: mobile-drawer-backdrop-out 200ms ease-in forwards; +} + +[data-slot="mobile-drawer-content"] { + box-sizing: border-box; + position: fixed; + inset-inline: 0; + bottom: 0; + z-index: 51; + display: flex; + flex-direction: column; + max-height: min(75dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 16px)); + padding: 0 12px max(12px, env(safe-area-inset-bottom, 0px)); + padding-left: max(12px, env(safe-area-inset-left, 0px)); + padding-right: max(12px, env(safe-area-inset-right, 0px)); + border-radius: 16px 16px 0 0; + background: var(--v2-background-bg-deep); + color: var(--v2-text-text-base); + box-shadow: var(--v2-elevation-overlay); + outline: none; + app-region: no-drag; +} + +[data-slot="mobile-drawer-content"][data-transitioning] { + transition: transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +[data-slot="mobile-drawer-content"][data-closing] { + transition-duration: 200ms; +} + +[data-slot="mobile-drawer-content"][data-closed] { + visibility: hidden; + pointer-events: none; +} + +[data-slot="mobile-drawer-handle"] { + display: flex; + height: 28px; + flex-shrink: 0; + align-items: center; + justify-content: center; + touch-action: none; +} + +[data-slot="mobile-drawer-handle"] span { + width: 32px; + height: 4px; + border-radius: 999px; + background: var(--v2-border-border-strong); +} + +@keyframes mobile-drawer-backdrop-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes mobile-drawer-backdrop-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + [data-slot="mobile-drawer-content"][data-transitioning], + [data-slot="mobile-drawer-content"][data-closing] { + transition: none; + } + + [data-slot="mobile-drawer-overlay"], + [data-slot="mobile-drawer-overlay"]:is([data-closing], [data-closed]) { + animation: none; + } +} diff --git a/packages/app/src/shell/mobile-drawer.tsx b/packages/app/src/shell/mobile-drawer.tsx new file mode 100644 index 00000000000..35f14f0d940 --- /dev/null +++ b/packages/app/src/shell/mobile-drawer.tsx @@ -0,0 +1,47 @@ +import Drawer from "@corvu/drawer" +import type { ParentProps } from "solid-js" +import { useLanguage } from "@/runtime/i18n/language" +import "./mobile-drawer.css" + +export function MobileDrawer( + props: ParentProps<{ + open: boolean + onOpenChange: (open: boolean) => void + onContentPresentChange?: (present: boolean) => void + returnFocus?: () => HTMLElement | undefined + closeOnOutsideFocus?: boolean + }>, +) { + return ( + + {props.children} + + ) +} + +export const MobileDrawerTrigger = Drawer.Trigger + +export function MobileDrawerContent(props: ParentProps) { + const language = useLanguage() + return ( + + + + + {props.children} + + + ) +} + +export const MobileDrawerLabel = Drawer.Label +export const MobileDrawerClose = Drawer.Close diff --git a/packages/app/src/shell/mobile-panel-drawer.css b/packages/app/src/shell/mobile-panel-drawer.css new file mode 100644 index 00000000000..53efedf627b --- /dev/null +++ b/packages/app/src/shell/mobile-panel-drawer.css @@ -0,0 +1,34 @@ +[data-slot="mobile-panel"] { + display: flex; + min-height: 0; + flex-direction: column; +} + +[data-slot="mobile-panel-header"] { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-inline-start: 8px; + padding-block-end: 8px; +} + +[data-slot="mobile-panel-header"] h2 { + margin: 0; + font-size: 14px; + font-weight: 530; + line-height: var(--line-height-base); +} + +[data-slot="mobile-panel-close"][data-component="button-v2"] { + height: 44px; + flex-shrink: 0; +} + +[data-slot="mobile-panel-content"] { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + touch-action: pan-y; +} diff --git a/packages/app/src/shell/mobile-panel-drawer.tsx b/packages/app/src/shell/mobile-panel-drawer.tsx index 1a8716cffde..fefbbaaf1da 100644 --- a/packages/app/src/shell/mobile-panel-drawer.tsx +++ b/packages/app/src/shell/mobile-panel-drawer.tsx @@ -1,7 +1,8 @@ -import Drawer from "@corvu/drawer" import type { ParentProps } from "solid-js" +import { Button } from "@opencode-ai/ui/button" import { useLanguage } from "@/runtime/i18n/language" -import "./status/status-drawer.css" +import { MobileDrawer, MobileDrawerClose, MobileDrawerContent, MobileDrawerLabel } from "./mobile-drawer" +import "./mobile-panel-drawer.css" export function MobilePanelDrawer( props: ParentProps<{ @@ -13,32 +14,29 @@ export function MobilePanelDrawer( ) { const language = useLanguage() return ( - - {/* Preserve Corvu's content and dismissal lifecycle across reopenings. */} - - - - -
- {props.title} - + +
+
+ {props.title} + {language.t("common.close")} - +
-
- {props.children} -
- - - +
{props.children}
+
+
+ ) } diff --git a/packages/app/src/shell/status/status-drawer.css b/packages/app/src/shell/status/status-drawer.css index 82aec4b0ec4..b50ebebb940 100644 --- a/packages/app/src/shell/status/status-drawer.css +++ b/packages/app/src/shell/status/status-drawer.css @@ -1,109 +1,3 @@ -[data-slot="mobile-status-overlay"] { - position: fixed; - inset: 0; - z-index: 50; - background: var(--v2-overlay-simple-overlay-scrim); - animation: mobile-status-backdrop-in 240ms ease-out; -} - -[data-slot="mobile-status-overlay"]:is([data-closing], [data-closed]) { - animation: mobile-status-backdrop-out 200ms ease-in forwards; -} - -[data-slot="mobile-status-drawer"] { - box-sizing: border-box; - position: fixed; - inset-inline: 0; - bottom: 0; - z-index: 51; - display: flex; - flex-direction: column; - max-height: min(75dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 16px)); - padding: 0 12px max(12px, env(safe-area-inset-bottom, 0px)); - padding-left: max(12px, env(safe-area-inset-left, 0px)); - padding-right: max(12px, env(safe-area-inset-right, 0px)); - border-radius: 16px 16px 0 0; - background: var(--v2-background-bg-deep); - color: var(--v2-text-text-base); - box-shadow: var(--v2-elevation-overlay); - outline: none; - app-region: no-drag; -} - -[data-slot="mobile-status-drawer"][data-transitioning] { - transition: transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1); -} - -[data-slot="mobile-status-drawer"][data-closing] { - transition-duration: 200ms; -} - -[data-slot="mobile-status-drawer"][data-closed] { - visibility: hidden; - pointer-events: none; -} - -[data-slot="mobile-status-drag-handle"] { - display: flex; - height: 28px; - flex-shrink: 0; - align-items: center; - justify-content: center; - touch-action: none; -} - -[data-slot="mobile-status-drag-handle"] span { - width: 32px; - height: 4px; - border-radius: 999px; - background: var(--v2-border-border-strong); -} - -[data-slot="mobile-status-header"] { - display: flex; - flex-shrink: 0; - align-items: center; - justify-content: space-between; - gap: 12px; - padding-inline-start: 8px; - padding-block-end: 8px; -} - -[data-slot="mobile-status-header"] h2 { - margin: 0; - font-size: 14px; - font-weight: 530; - line-height: var(--line-height-base); -} - -[data-slot="mobile-status-close"] { - min-height: 44px; - flex-shrink: 0; - padding-inline: 12px; - border-radius: 6px; - color: var(--v2-text-text-base); - font-size: 13px; - line-height: var(--line-height-compact); -} - -@media (hover: hover) { - [data-slot="mobile-status-close"]:hover { - background: var(--v2-overlay-simple-overlay-hover); - } -} - -[data-slot="mobile-status-close"]:focus-visible { - outline: 2px solid var(--v2-border-border-focus); - outline-offset: -2px; -} - -[data-slot="mobile-status-content"] { - min-height: 0; - overflow-y: auto; - overscroll-behavior: contain; - touch-action: pan-y; -} - [data-slot="mobile-status-loading"] { display: flex; min-height: 56px; @@ -113,33 +7,3 @@ font-size: 13px; line-height: var(--line-height-base); } - -@keyframes mobile-status-backdrop-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes mobile-status-backdrop-out { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -@media (prefers-reduced-motion: reduce) { - [data-slot="mobile-status-drawer"][data-transitioning], - [data-slot="mobile-status-drawer"][data-closing] { - transition: none; - } - - [data-slot="mobile-status-overlay"], - [data-slot="mobile-status-overlay"]:is([data-closing], [data-closed]) { - animation: none; - } -} diff --git a/packages/app/src/shell/status/status-drawer.tsx b/packages/app/src/shell/status/status-drawer.tsx index 0630ededae7..2427e6bc268 100644 --- a/packages/app/src/shell/status/status-drawer.tsx +++ b/packages/app/src/shell/status/status-drawer.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense } from "solid-js" import { useLanguage } from "@/runtime/i18n/language" import { MobilePanelDrawer } from "../mobile-panel-drawer" +import "./status-drawer.css" const Body = lazy(async () => { const { StatusPopoverBody } = await import("./body") diff --git a/packages/app/src/shell/titlebar/titlebar.css b/packages/app/src/shell/titlebar/titlebar.css index 4af625694b3..42e471d8c8e 100644 --- a/packages/app/src/shell/titlebar/titlebar.css +++ b/packages/app/src/shell/titlebar/titlebar.css @@ -14,63 +14,13 @@ var(--v2-background-bg-layer-02); } -[data-slot="mobile-tabs-overlay"] { - position: fixed; - inset: 0; - z-index: 50; - background: var(--v2-overlay-simple-overlay-scrim); - animation: mobile-tabs-backdrop-in 240ms ease-out; -} - -[data-slot="mobile-tabs-overlay"]:is([data-closing], [data-closed]) { - animation: mobile-tabs-backdrop-out 200ms ease-in forwards; -} - /* Keep the strip mounted for tab shortcuts and session metadata while collapsed. */ [data-slot="mobile-tabs-drawer"] { - box-sizing: border-box; - position: fixed; - inset-inline: 0; - bottom: 0; - z-index: 51; display: flex; + min-height: 0; flex-direction: column; gap: 8px; - max-height: min(75dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 16px)); - padding: 0 12px max(12px, env(safe-area-inset-bottom, 0px)); - border-radius: 16px 16px 0 0; - background: var(--v2-background-bg-deep); - box-shadow: var(--v2-elevation-overlay); - outline: none; -} - -[data-slot="mobile-tabs-drawer"][data-transitioning] { - transition: transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1); -} - -[data-slot="mobile-tabs-drawer"][data-closing] { - transition-duration: 200ms; -} - -[data-slot="mobile-tabs-drawer"][data-closed] { - visibility: hidden; - pointer-events: none; -} - -[data-slot="mobile-tabs-drag-handle"] { - display: flex; - height: 28px; - flex-shrink: 0; - align-items: center; - justify-content: center; - touch-action: none; -} - -[data-slot="mobile-tabs-drag-handle"] span { - width: 32px; - height: 4px; - border-radius: 999px; - background: var(--v2-border-border-strong); + margin-block-start: 8px; } [data-slot="mobile-tabs-drawer-list"] { @@ -79,36 +29,6 @@ flex-direction: column; } -@keyframes mobile-tabs-backdrop-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes mobile-tabs-backdrop-out { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -@media (prefers-reduced-motion: reduce) { - [data-slot="mobile-tabs-drawer"][data-transitioning], - [data-slot="mobile-tabs-drawer"][data-closing] { - transition: none; - } - - [data-slot="mobile-tabs-overlay"], - [data-slot="mobile-tabs-overlay"]:is([data-closing], [data-closed]) { - animation: none; - } -} - [data-slot="mobile-tabs-drawer"] [data-slot="vertical-tabs"] { display: flex; flex-direction: column; diff --git a/packages/app/src/shell/titlebar/titlebar.tsx b/packages/app/src/shell/titlebar/titlebar.tsx index 975dde00e0a..87f95c19c40 100644 --- a/packages/app/src/shell/titlebar/titlebar.tsx +++ b/packages/app/src/shell/titlebar/titlebar.tsx @@ -25,7 +25,7 @@ import type { ComposerState } from "@/composer/persistence" import "./titlebar.css" import { newTabTooltipKeybind } from "@/shell/commands/tooltip-keybind" import { TitlebarRightMount } from "@/shell/titlebar/right-slot" -import Drawer from "@corvu/drawer" +import { MobileDrawer, MobileDrawerContent, MobileDrawerLabel, MobileDrawerTrigger } from "@/shell/mobile-drawer" import { sessionLabel } from "@/session/title" import { SessionTabAvatar } from "@/shell/layout/session-tab-avatar" import { projectForSession } from "@/shell/layout/helpers" @@ -415,7 +415,7 @@ export function Titlebar(props: { setMobileTabs("open", open)} onContentPresentChange={(present) => { @@ -423,11 +423,9 @@ export function Titlebar(props: { setMobileTabs("settings", false) openSettings() }} - side="bottom" > - @@ -467,15 +465,11 @@ export function Titlebar(props: { {currentTitle()} {tabsStore.length} - - - - - {language.t("titlebar.tabs")} - -
+ + + {language.t("titlebar.tabs")} +
+
-
+
- - - +
+ + } >