feat(ui): add responsive layout support for mobile and small screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-07 15:47:16 +08:00
parent dd659dcaa5
commit 768d1326b1
17 changed files with 664 additions and 216 deletions

View File

@@ -1493,7 +1493,7 @@ export function MessageInput({
onPaste={handlePaste}
onFocus={onFocus}
placeholder={resolvedPlaceholder}
className="min-h-0 flex-1 overflow-y-auto rounded-none border-0 bg-transparent text-sm shadow-none focus-visible:border-0 focus-visible:ring-0"
className="min-h-0 flex-1 overflow-y-auto rounded-none border-0 bg-transparent text-base md:text-sm shadow-none focus-visible:border-0 focus-visible:ring-0"
autoFocus={autoFocus}
/>
<div className="@container flex shrink-0 items-end justify-between gap-2 px-2 pb-2">

View File

@@ -2,6 +2,7 @@
import type { ReactNode } from "react"
import { usePlatform } from "@/hooks/use-platform"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { WindowControls } from "./window-controls"
@@ -25,6 +26,7 @@ export function AppTitleBar({
showWindowControls = true,
}: AppTitleBarProps) {
const { isMac, isWindows } = usePlatform()
const isMobile = useIsMobile()
const isDesktopRuntime =
typeof window !== "undefined" && "__TAURI_INTERNALS__" in window
const hasDesktopWindowChrome = showWindowControls && isDesktopRuntime
@@ -38,7 +40,8 @@ export function AppTitleBar({
return (
<div
className={cn(
"relative h-8 shrink-0 border-b bg-muted/70 select-none",
"relative shrink-0 border-b bg-muted/70 select-none",
isMobile ? "h-11" : "h-8",
className
)}
>

View File

@@ -9,7 +9,9 @@ import {
} from "react"
import {
Columns2,
EllipsisVertical,
FileCode2,
Menu,
MessageSquare,
PanelLeft,
PanelRight,
@@ -40,6 +42,13 @@ import { CommandDropdown } from "./command-dropdown"
import { SearchCommandDialog } from "@/components/conversations/search-command-dialog"
import { DirectoryBrowserDialog } from "@/components/shared/directory-browser-dialog"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
const MODE_TABS = [
{
@@ -196,6 +205,7 @@ export function FolderTitleBar() {
setBranch(null)
}
}, [folderPath])
const isMobile = useIsMobile()
const modeContainerRef = useRef<HTMLDivElement>(null)
const modeItemRefs = useRef<Map<string, HTMLDivElement>>(new Map())
const [modeIndicator, setModeIndicator] = useState<{
@@ -243,168 +253,234 @@ export function FolderTitleBar() {
[setMode]
)
const modeTabsElement = (
<div
ref={modeContainerRef}
role="tablist"
aria-label={tModes("workspaceModesAria")}
className="relative inline-flex h-[27px] items-center rounded-full border border-border/50 bg-muted/50 p-0.5"
>
{modeIndicator && (
<div
className="pointer-events-none absolute top-0.5 bottom-0.5 rounded-full bg-background shadow-sm ring-1 ring-border/50 transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
style={{
left: modeIndicator.left,
width: modeIndicator.width,
}}
/>
)}
{MODE_TABS.map((item) => {
const Icon = item.icon
const isActive = mode === item.mode
const title = tModes(item.titleKey)
return (
<div
key={item.mode}
ref={(el) => {
if (el) {
modeItemRefs.current.set(item.mode, el)
} else {
modeItemRefs.current.delete(item.mode)
}
}}
role="tab"
tabIndex={0}
className={cn(
"relative z-10 m-0 flex h-[23px] cursor-pointer select-none items-center justify-center gap-1 rounded-full border-0 bg-transparent p-0 align-middle text-xs font-medium leading-none transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
isActive ? "px-2.5" : "px-2",
isActive
? "text-foreground"
: "text-muted-foreground hover:text-foreground/70"
)}
onClick={() => setMode(item.mode)}
onKeyDown={(event) => handleModeKeyDown(event, item.mode)}
onMouseDown={(event) => event.preventDefault()}
title={!isActive ? title : undefined}
aria-label={title}
aria-selected={isActive}
>
<Icon
className="block h-3 w-3 shrink-0"
shapeRendering="geometricPrecision"
/>
{/* Hide text labels on mobile to save space */}
{!isMobile && (
<span
className={cn(
"grid transition-[grid-template-columns] duration-300",
isActive ? "grid-cols-[1fr]" : "grid-cols-[0fr]"
)}
>
<span
className={cn(
"min-w-0 overflow-hidden whitespace-nowrap transition-opacity duration-300",
isActive ? "opacity-100" : "opacity-0"
)}
>
{title}
</span>
</span>
)}
</div>
)
})}
</div>
)
return (
<>
<AppTitleBar
centerInteractive
left={
<div className="flex min-w-0 items-center gap-4">
<FolderNameDropdown />
<BranchDropdown
branch={branch}
parentBranch={folder?.parent_branch ?? null}
onBranchChange={refreshBranch}
/>
<div data-tauri-drag-region className="h-8 flex-1" />
</div>
}
center={
<div
ref={modeContainerRef}
role="tablist"
aria-label={tModes("workspaceModesAria")}
className="relative inline-flex h-[27px] items-center rounded-full border border-border/50 bg-muted/50 p-0.5"
>
{modeIndicator && (
<div
className="pointer-events-none absolute top-0.5 bottom-0.5 rounded-full bg-background shadow-sm ring-1 ring-border/50 transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
style={{
left: modeIndicator.left,
width: modeIndicator.width,
}}
/>
)}
{MODE_TABS.map((item) => {
const Icon = item.icon
const isActive = mode === item.mode
const title = tModes(item.titleKey)
return (
<div
key={item.mode}
ref={(el) => {
if (el) {
modeItemRefs.current.set(item.mode, el)
} else {
modeItemRefs.current.delete(item.mode)
}
}}
role="tab"
tabIndex={0}
className={cn(
"relative z-10 m-0 flex h-[23px] cursor-pointer select-none items-center justify-center gap-1 rounded-full border-0 bg-transparent p-0 align-middle text-xs font-medium leading-none transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
isActive ? "px-2.5" : "px-2",
isActive
? "text-foreground"
: "text-muted-foreground hover:text-foreground/70"
)}
onClick={() => setMode(item.mode)}
onKeyDown={(event) => handleModeKeyDown(event, item.mode)}
onMouseDown={(event) => event.preventDefault()}
title={!isActive ? title : undefined}
aria-label={title}
aria-selected={isActive}
>
<Icon
className="block h-3 w-3 shrink-0"
shapeRendering="geometricPrecision"
/>
<span
className={cn(
"grid transition-[grid-template-columns] duration-300",
isActive ? "grid-cols-[1fr]" : "grid-cols-[0fr]"
)}
>
<span
className={cn(
"min-w-0 overflow-hidden whitespace-nowrap transition-opacity duration-300",
isActive ? "opacity-100" : "opacity-0"
)}
>
{title}
</span>
</span>
</div>
)
})}
</div>
}
right={
<div className="flex items-center gap-10">
<div className="flex items-center gap-2">
<CommandDropdown />
</div>
<div className="flex items-center gap-2">
isMobile ? (
<div className="flex min-w-0 items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
className="h-8 w-8 shrink-0"
onClick={toggle}
title={tTitleBar("withShortcut", {
label: tTitleBar(isOpen ? "hideSidebar" : "showSidebar"),
shortcut: formatShortcutLabel(
shortcuts.toggle_sidebar,
isMac
),
})}
>
<PanelLeft className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${auxPanelOpen ? "bg-accent" : ""}`}
onClick={toggleAuxPanel}
title={tTitleBar("withShortcut", {
label: tTitleBar("toggleAuxPanel"),
shortcut: formatShortcutLabel(
shortcuts.toggle_aux_panel,
isMac
),
})}
>
<PanelRight className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${terminalOpen ? "bg-accent" : ""}`}
onClick={() => toggleTerminal()}
title={tTitleBar("withShortcut", {
label: tTitleBar("toggleTerminal"),
shortcut: formatShortcutLabel(
shortcuts.toggle_terminal,
isMac
),
})}
>
<SquareTerminal className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={() => setSearchOpen(true)}
title={tTitleBar("withShortcut", {
label: tTitleBar("search"),
shortcut: formatShortcutLabel(shortcuts.toggle_search, isMac),
})}
>
<Search className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={handleOpenSettings}
title={tTitleBar("withShortcut", {
label: tTitleBar("openSettings"),
shortcut: formatShortcutLabel(shortcuts.open_settings, isMac),
})}
>
<Settings className="h-3.5 w-3.5" />
<Menu className="h-4 w-4" />
</Button>
<FolderNameDropdown />
<BranchDropdown
branch={branch}
parentBranch={folder?.parent_branch ?? null}
onBranchChange={refreshBranch}
/>
</div>
</div>
) : (
<div className="flex min-w-0 items-center gap-4">
<FolderNameDropdown />
<BranchDropdown
branch={branch}
parentBranch={folder?.parent_branch ?? null}
onBranchChange={refreshBranch}
/>
<div data-tauri-drag-region className="h-8 flex-1" />
</div>
)
}
center={isMobile ? undefined : modeTabsElement}
right={
isMobile ? (
<div className="flex items-center gap-1">
<CommandDropdown />
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setSearchOpen(true)}
title={tTitleBar("search")}
>
<Search className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<EllipsisVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={toggleAuxPanel}>
<PanelRight className="h-3.5 w-3.5" />
{tTitleBar("toggleAuxPanel")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toggleTerminal()}>
<SquareTerminal className="h-3.5 w-3.5" />
{tTitleBar("toggleTerminal")}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenSettings}>
<Settings className="h-3.5 w-3.5" />
{tTitleBar("openSettings")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<div className="flex items-center gap-10">
<div className="flex items-center gap-2">
<CommandDropdown />
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={toggle}
title={tTitleBar("withShortcut", {
label: tTitleBar(isOpen ? "hideSidebar" : "showSidebar"),
shortcut: formatShortcutLabel(
shortcuts.toggle_sidebar,
isMac
),
})}
>
<PanelLeft className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${auxPanelOpen ? "bg-accent" : ""}`}
onClick={toggleAuxPanel}
title={tTitleBar("withShortcut", {
label: tTitleBar("toggleAuxPanel"),
shortcut: formatShortcutLabel(
shortcuts.toggle_aux_panel,
isMac
),
})}
>
<PanelRight className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${terminalOpen ? "bg-accent" : ""}`}
onClick={() => toggleTerminal()}
title={tTitleBar("withShortcut", {
label: tTitleBar("toggleTerminal"),
shortcut: formatShortcutLabel(
shortcuts.toggle_terminal,
isMac
),
})}
>
<SquareTerminal className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={() => setSearchOpen(true)}
title={tTitleBar("withShortcut", {
label: tTitleBar("search"),
shortcut: formatShortcutLabel(
shortcuts.toggle_search,
isMac
),
})}
>
<Search className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={handleOpenSettings}
title={tTitleBar("withShortcut", {
label: tTitleBar("openSettings"),
shortcut: formatShortcutLabel(
shortcuts.open_settings,
isMac
),
})}
>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)
}
/>
<SearchCommandDialog open={searchOpen} onOpenChange={setSearchOpen} />

View File

@@ -11,12 +11,14 @@ import {
type SidebarConversationListHandle,
} from "@/components/conversations/sidebar-conversation-list"
import { Button } from "@/components/ui/button"
import { useIsMobile } from "@/hooks/use-mobile"
export function Sidebar() {
const t = useTranslations("Folder.sidebar")
const { folder } = useFolderContext()
const { openNewConversationTab } = useTabContext()
const { isOpen } = useSidebarContext()
const { isOpen, toggle } = useSidebarContext()
const isMobile = useIsMobile()
const listRef = useRef<SidebarConversationListHandle>(null)
const handleNewConversation = useCallback(() => {
@@ -70,7 +72,22 @@ export function Sidebar() {
</div>
</div>
<SidebarConversationList ref={listRef} />
{/* On mobile, clicking a conversation card auto-closes the Sheet */}
<div
className="flex-1 min-h-0 overflow-hidden"
onClick={
isMobile
? (e) => {
const target = e.target as HTMLElement
if (target.closest("[data-conversation-id]")) {
toggle()
}
}
: undefined
}
>
<SidebarConversationList ref={listRef} />
</div>
</aside>
)
}

View File

@@ -6,8 +6,23 @@ import { StatusBarTasks } from "@/components/layout/status-bar-tasks"
import { StatusBarTokens } from "@/components/layout/status-bar-tokens"
import { StatusBarConnection } from "@/components/layout/status-bar-connection"
import { StatusBarAlerts } from "@/components/layout/status-bar-alerts"
import { useIsMobile } from "@/hooks/use-mobile"
export function StatusBar() {
const isMobile = useIsMobile()
if (isMobile) {
return (
<div className="h-7 shrink-0 border-t border-border bg-muted px-3 flex items-center justify-between text-xs text-muted-foreground">
<StatusBarConnection />
<div className="flex items-center gap-3">
<StatusBarTasks />
<StatusBarAlerts />
</div>
</div>
)
}
return (
<div className="h-8 shrink-0 border-t border-border bg-muted px-4 flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center">

View File

@@ -3,6 +3,7 @@
import {
useCallback,
useEffect,
useState,
type ComponentType,
type ReactNode,
} from "react"
@@ -12,6 +13,7 @@ import {
GitBranch,
Globe,
Keyboard,
Menu,
SendHorizontal,
Palette,
PlugZap,
@@ -26,6 +28,8 @@ import { AppToaster } from "@/components/ui/app-toaster"
import { cn } from "@/lib/utils"
import { detectEnvironment } from "@/lib/transport/detect"
import { AppTitleBar } from "@/components/layout/app-title-bar"
import { useIsMobile } from "@/hooks/use-mobile"
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"
interface SettingsNavItem {
href: string
@@ -118,6 +122,8 @@ export function SettingsShell({ children }: SettingsShellProps) {
const pathname = usePathname()
const router = useRouter()
const normalizedPathname = normalizePath(pathname)
const isMobile = useIsMobile()
const [navOpen, setNavOpen] = useState(false)
useEffect(() => {
document.title = `${t("title")} - codeg`
@@ -129,67 +135,108 @@ export function SettingsShell({ children }: SettingsShellProps) {
const target = normalizePath(href)
const current = normalizePath(window.location.pathname)
if (current === target) return
if (current === target) {
setNavOpen(false)
return
}
if (isWindowsRuntime()) {
// WebView2 on Windows: hard navigation is more reliable than client routing.
window.location.assign(target)
return
}
// macOS/Linux: keep client-side routing for snappier transitions.
router.push(target)
setNavOpen(false)
},
[router]
[router, setNavOpen]
)
const filteredNavItems = SETTINGS_NAV_ITEMS.filter(
(item) =>
!(item.labelKey === "web_service" && detectEnvironment() === "web")
)
const navContent = (
<>
<div className="px-1 pb-2 text-[11px] font-medium text-muted-foreground">
{t("preferences")}
</div>
<nav className="space-y-1">
{filteredNavItems.map((item) => {
const Icon = item.icon
const translationKey = `nav.${item.labelKey}` as const
const active =
normalizedPathname === item.href ||
normalizedPathname.startsWith(`${item.href}/`)
return (
<Button
key={item.href}
variant={active ? "secondary" : "ghost"}
size="sm"
className={cn("w-full justify-start")}
type="button"
onClick={() => navigateTo(item.href)}
aria-current={active ? "page" : undefined}
>
<span className="inline-flex items-center gap-1">
<Icon className="h-3.5 w-3.5" />
{t(translationKey)}
</span>
</Button>
)
})}
</nav>
</>
)
return (
<div className="h-screen flex flex-col overflow-hidden bg-background text-foreground">
<AppTitleBar
left={
isMobile ? (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setNavOpen(true)}
>
<Menu className="h-4 w-4" />
</Button>
) : undefined
}
center={
<div className="text-sm font-bold tracking-tight">{t("title")}</div>
}
/>
<div className="flex-1 min-h-0 flex">
<aside className="w-56 shrink-0 border-r p-3">
<div className="px-1 pb-2 text-[11px] font-medium text-muted-foreground">
{t("preferences")}
</div>
<nav className="space-y-1">
{SETTINGS_NAV_ITEMS.filter(
(item) =>
!(
item.labelKey === "web_service" &&
detectEnvironment() === "web"
)
).map((item) => {
const Icon = item.icon
const translationKey = `nav.${item.labelKey}` as const
const active =
normalizedPathname === item.href ||
normalizedPathname.startsWith(`${item.href}/`)
return (
<Button
key={item.href}
variant={active ? "secondary" : "ghost"}
size="sm"
className={cn("w-full justify-start")}
type="button"
onClick={() => navigateTo(item.href)}
aria-current={active ? "page" : undefined}
>
<span className="inline-flex items-center gap-1">
<Icon className="h-3.5 w-3.5" />
{t(translationKey)}
</span>
</Button>
)
})}
</nav>
</aside>
{/* Desktop sidebar */}
{!isMobile && (
<aside className="w-56 shrink-0 border-r p-3">{navContent}</aside>
)}
<section className="flex-1 min-w-0 min-h-0 p-4">{children}</section>
{/* Mobile navigation Sheet */}
{isMobile && (
<Sheet open={navOpen} onOpenChange={setNavOpen}>
<SheetContent
side="left"
showCloseButton={false}
className="w-[260px] p-3"
>
<SheetTitle className="sr-only">{t("title")}</SheetTitle>
{navContent}
</SheetContent>
</Sheet>
)}
<section
className={cn(
"flex-1 min-w-0 min-h-0 overflow-auto",
isMobile ? "p-3" : "p-4"
)}
>
{children}
</section>
</div>
<AppToaster position="bottom-right" closeButton duration={4000} />
</div>

View File

@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-4xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
"group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
@@ -15,9 +15,9 @@ const buttonVariants = cva(
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {

View File

@@ -34,7 +34,7 @@ function CommandDialog({
}) {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 rounded-2xl max-w-lg">
<DialogContent className="overflow-hidden p-0 rounded-2xl max-w-lg md:max-w-xl lg:max-w-2xl">
<VisuallyHidden.Root>
<DialogTitle>{title}</DialogTitle>
</VisuallyHidden.Root>

143
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import { Dialog as SheetPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" asChild>
<Button
variant="ghost"
className="absolute top-4 right-4"
size="icon-sm"
>
<XIcon />
<span className="sr-only">Close</span>
</Button>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-6", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-6", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-base font-medium text-foreground", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -15,11 +15,13 @@ import { Button } from "@/components/ui/button"
import { AppToaster } from "@/components/ui/app-toaster"
import { resolveWelcomeError } from "@/components/welcome/error-utils"
import { AppTitleBar } from "@/components/layout/app-title-bar"
import { useIsMobile } from "@/hooks/use-mobile"
export function WelcomeScreen() {
const t = useTranslations("WelcomePage")
const [history, setHistory] = useState<FolderHistoryEntry[]>([])
const [loading, setLoading] = useState(true)
const isMobile = useIsMobile()
const { shortcuts } = useShortcutSettings()
const handleOpenSettings = useCallback(() => {
@@ -83,8 +85,20 @@ export function WelcomeScreen() {
}
/>
<div className="flex-1 flex overflow-hidden">
<div className="w-60 shrink-0 flex flex-col border-r">
<div
className={
isMobile
? "flex-1 flex flex-col overflow-hidden"
: "flex-1 flex overflow-hidden"
}
>
<div
className={
isMobile
? "shrink-0 flex flex-col border-b"
: "w-60 shrink-0 flex flex-col border-r"
}
>
<SoftwareInfo />
<FolderActions />
</div>