Files
claude-code-haha/desktop/src/components/browser/BrowserAddressBar.tsx
T
程序员阿江(Relakkes) c712f52858 feat(desktop): 「纸·墨·印」全量 UI 重设计
按设计交付稿重构桌面端整体视觉,只换表现层,功能、数据契约与
交互逻辑零改动。

主题与地基:
- 6 套主题(纯白/纸墨/经典暖色/青瓷/墨夜/墨夜蓝),源色块 + 单一
  语义映射层两层结构;老设置 light→warm-classic 自动迁移
- 状态色全部成对并过 AA;新增 --color-on-brand-soft、
  --color-search-highlight 对、--color-border-strong(1.4.11 控件边界)
- 衬线标题自托管 Noto Serif SC 拉丁分片,中文回退系统衬线
- 圆角/阴影/动效阶梯按稿定稿;新组件 BrandSeal 印章

屏幕:外壳(52px 标签条陶土下划线、印章侧栏、选中会话浮起卡)、
零态、活跃会话、composer 全部弹层、技能市场、定时任务、设置全
分区、Trace、终端、⌘K,及设计稿未覆盖界面的统一收敛。

Review 修复(两轮 6 主题全量走查 + 盲区补扫挖出):
- 未分层 CSS 压死 utilities 两处(图标 24px、focus ring 被吃)
- font-[var(--font-mono)] 无效写法 35 处(mono 从未生效)
- tertiary 文字与控件边框对比度全线达标化,contrast 守门扩容
- /N alpha 修饰符清零(Safari 15 WebView 整条丢弃)
- 触屏 44px 主目标口径统一;hover 门控控件 H5 常显
- 错误态修复:定时任务静默失败、Trace/会话列表错误呈现
- 宠物窗口 ink-blue 暗色覆盖补齐

测试:264 文件 / 3162 用例全绿(新增六主题对比度、跨文件弹层
一致性、触摸目标、错误态等守门);tsc 零报错;构建通过。
2026-07-26 22:54:45 +08:00

78 lines
3.3 KiB
TypeScript

import { useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import { ArrowLeft, ArrowRight, RotateCw } from 'lucide-react'
import { IconButton } from '@/components/ui/IconButton'
import { Spinner } from '@/components/ui/Spinner'
import { useTranslation } from '../../i18n'
import { isHtmlFilePath } from '../../lib/htmlPreviewPolicy'
type Props = {
url: string
canGoBack: boolean
canGoForward: boolean
loading?: boolean
onNavigate: (url: string) => void
onBack: () => void
onForward: () => void
onReload: () => void
rightActions?: ReactNode
}
export function BrowserAddressBar({ url, canGoBack, canGoForward, loading = false, onNavigate, onBack, onForward, onReload, rightActions }: Props) {
const t = useTranslation()
const [draft, setDraft] = useState(url)
useEffect(() => { setDraft(url) }, [url])
return (
<div
data-testid="browser-address-bar"
className="relative flex h-11 items-center gap-1 border-b border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-2"
>
<IconButton icon={<ArrowLeft size={16} />} label={t('browser.back')} size="xs" disabled={!canGoBack} onClick={onBack} />
<IconButton icon={<ArrowRight size={16} />} label={t('browser.forward')} size="xs" disabled={!canGoForward} onClick={onForward} />
{/* `aria-busy` stays explicit: the reload button must remain clickable while
loading, so IconButton's `loading` prop (which disables) is not used. */}
<IconButton
icon={loading ? <Spinner size={16} /> : <RotateCw size={16} />}
label={t('browser.reload')}
size="xs"
aria-busy={loading}
onClick={onReload}
/>
<form className="min-w-0 flex-1" onSubmit={(e) => { e.preventDefault(); onNavigate(normalizeBrowserAddress(draft)) }}>
<input
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 py-1 font-mono text-xs text-[var(--color-text-primary)] outline-none transition-[border-color,box-shadow] duration-150 ease-out placeholder:font-[var(--font-body)] placeholder:text-[var(--color-text-tertiary)] hover:border-[var(--color-outline)] focus-visible:border-[var(--color-border-focus)] focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder={t('browser.addressPlaceholder')}
spellCheck={false}
/>
</form>
{rightActions && (
<div data-testid="browser-toolbar-actions" className="ml-1 flex shrink-0 items-center gap-1">
{rightActions}
</div>
)}
{loading && (
<div
role="progressbar"
aria-label={t('browser.loading')}
data-testid="browser-loading-bar"
className="progress-indeterminate-track pointer-events-none absolute inset-x-0 bottom-0 h-0.5"
/>
)}
</div>
)
}
export function normalizeBrowserAddress(input: string): string {
const value = input.trim()
if (!value) return ''
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value) || /^(about|data|file):/i.test(value)) return value
if (isHtmlFilePath(value)) return value
if (/^(localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)(?::\d+)?(?:[/?#].*)?$/i.test(value)) {
return `http://${value}`
}
return `https://${value}`
}