mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
c712f52858
按设计交付稿重构桌面端整体视觉,只换表现层,功能、数据契约与 交互逻辑零改动。 主题与地基: - 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 零报错;构建通过。
78 lines
3.3 KiB
TypeScript
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}`
|
|
}
|