mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
H5 browser use needs to keep local desktop WebUI testing open while giving remote phones a usable chat-first surface. This change scopes browser auth by origin and target host, hides non-chat desktop chrome on H5 mobile, compacts the empty-session composer, and unifies mobile selectors behind a full-width bottom sheet with an explicit close action. Constraint: Local browser development may bypass H5 auth only for loopback or private LAN server hosts; public or remote hosts still require an H5 token. Constraint: Mobile H5 scope is chat-first; settings and scheduled-task surfaces stay hidden instead of fully redesigned. Rejected: Reuse desktop popovers on mobile | hover/anchored dropdown behavior leaves key actions hard to dismiss and cramped on narrow screens. Rejected: Let any localhost origin bypass remote auth | it would let arbitrary local pages access public H5 servers without a token. Confidence: medium Scope-risk: moderate Directive: Keep future H5 selector popups on MobileBottomSheet unless a selector needs a reviewed custom mobile interaction. Tested: cd desktop && bun run lint Tested: cd desktop && bun run test -- src/pages/EmptySession.test.tsx src/components/chat/ChatInput.test.tsx src/lib/desktopRuntime.test.ts src/components/shared/RepositoryLaunchControls.test.tsx src/components/controls/PermissionModeSelector.test.tsx src/components/controls/ModelSelector.test.tsx src/components/shared/DirectoryPicker.test.tsx src/__tests__/pages.test.tsx Tested: bun test src/server/__tests__/h5-access-auth.test.ts src/server/middleware/cors.test.ts Tested: Chrome mobile viewport smoke for context, model, permission, project, branch, and worktree bottom sheets with no horizontal overflow. Not-tested: Full bun run verify quality gate.
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import { useEffect, type ReactNode, type Ref } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
|
|
type Props = {
|
|
open: boolean
|
|
onClose: () => void
|
|
title: ReactNode
|
|
children: ReactNode
|
|
closeLabel?: string
|
|
headerExtra?: ReactNode
|
|
footer?: ReactNode
|
|
id?: string
|
|
role?: string
|
|
ariaLabel?: string
|
|
contentClassName?: string
|
|
panelClassName?: string
|
|
panelRef?: Ref<HTMLDivElement>
|
|
testId?: string
|
|
}
|
|
|
|
export function MobileBottomSheet({
|
|
open,
|
|
onClose,
|
|
title,
|
|
children,
|
|
closeLabel = 'Close',
|
|
headerExtra,
|
|
footer,
|
|
id,
|
|
role = 'dialog',
|
|
ariaLabel,
|
|
contentClassName = '',
|
|
panelClassName = '',
|
|
panelRef,
|
|
testId,
|
|
}: Props) {
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') onClose()
|
|
}
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
}, [open, onClose])
|
|
|
|
if (!open) return null
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-[10000] bg-black/25" onClick={onClose}>
|
|
<div
|
|
ref={panelRef}
|
|
id={id}
|
|
role={role}
|
|
aria-modal={role === 'dialog' ? true : undefined}
|
|
aria-label={ariaLabel ?? (typeof title === 'string' ? title : undefined)}
|
|
data-testid={testId}
|
|
className={`absolute inset-x-0 bottom-0 flex max-h-[min(78dvh,640px)] min-h-0 flex-col overflow-hidden rounded-t-2xl border-x-0 border-y border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] shadow-[0_-18px_48px_rgba(54,35,28,0.22)] ${panelClassName}`}
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="shrink-0 border-b border-[var(--color-border)] px-4 py-3">
|
|
<div className="flex min-h-10 items-center justify-between gap-3">
|
|
<div className="min-w-0 text-[11px] font-bold uppercase tracking-widest text-[var(--color-outline)]">
|
|
{title}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
aria-label={closeLabel}
|
|
onClick={onClose}
|
|
className="grid h-10 w-10 shrink-0 place-items-center rounded-full text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
|
|
>
|
|
<span className="material-symbols-outlined text-[20px]">close</span>
|
|
</button>
|
|
</div>
|
|
{headerExtra && (
|
|
<div className="mt-3">
|
|
{headerExtra}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={`min-h-0 flex-1 overflow-y-auto ${contentClassName}`}>
|
|
{children}
|
|
</div>
|
|
|
|
{footer && (
|
|
<div className="shrink-0 border-t border-[var(--color-border)]">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
}
|