feat(polaris): 完善首页背景与搜索入口视觉

使用本地背景和官方 Provider 图标,闭合状态仅展示当前图标。
调整冷蓝磨砂层次,并移除快捷卡片外部阴影以消除底部色带。
补充搜索 Provider 展示行为测试。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 22:25:30 +08:00
co-authored by Claude Opus 4.8
parent 9754d61ffe
commit 5a46186bde
9 changed files with 132 additions and 80 deletions
+77 -60
View File
@@ -20,15 +20,33 @@
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-soft: rgba(59, 130, 246, 0.1);
--surface-sidebar: rgba(255, 255, 255, 0.68);
--surface-search: rgba(255, 255, 255, 0.72);
--surface-card: rgba(255, 255, 255, 0.62);
--surface-card-hover: rgba(255, 255, 255, 0.78);
--border-glass: rgba(255, 255, 255, 0.58);
--surface-sidebar: linear-gradient(
180deg,
rgba(235, 243, 251, 0.52),
rgba(204, 220, 238, 0.34)
);
--surface-search: linear-gradient(
135deg,
rgba(240, 246, 252, 0.56),
rgba(210, 225, 241, 0.4)
);
--surface-card: linear-gradient(
135deg,
rgba(231, 240, 249, 0.43),
rgba(198, 216, 236, 0.32)
);
--surface-card-hover: linear-gradient(
135deg,
rgba(242, 248, 253, 0.6),
rgba(216, 230, 244, 0.48)
);
--border-glass: rgba(255, 255, 255, 0.38);
--border-subtle: rgba(15, 23, 42, 0.08);
--shadow-soft: 0 14px 40px rgba(15, 23, 42, 0.1);
--shadow-medium: 0 20px 60px rgba(15, 23, 42, 0.12);
--shadow-panel: 0 24px 80px rgba(15, 23, 42, 0.1);
--shadow-soft: inset 0 1px 0 rgba(255, 255, 255, 0.34);
--shadow-medium:
0 22px 58px rgba(4, 12, 34, 0.17), inset 0 1px 0 rgba(255, 255, 255, 0.4);
--shadow-panel:
0 28px 72px rgba(4, 12, 34, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.42);
--radius-card: 20px;
--radius-panel: 24px;
--sidebar-expanded: 240px;
@@ -50,23 +68,12 @@ body {
margin: 0;
overflow-x: hidden;
color: var(--text-primary);
background:
radial-gradient(
circle at 12% 15%,
rgba(255, 245, 239, 0.92) 0,
transparent 34%
),
radial-gradient(
circle at 82% 14%,
rgba(194, 222, 246, 0.92) 0,
transparent 38%
),
radial-gradient(
circle at 48% 82%,
rgba(226, 239, 247, 0.72) 0,
transparent 44%
),
linear-gradient(135deg, #eef5fa 0%, #dceaf4 48%, #cbdff0 100%);
background-color: #0b102b;
background-image: url("/backgrounds/test.jpg");
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
font-family:
Inter,
"SF Pro Display",
@@ -86,19 +93,14 @@ body::before {
z-index: -1;
content: "";
pointer-events: none;
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0.08)
),
repeating-linear-gradient(
165deg,
transparent 0 84px,
rgba(255, 255, 255, 0.08) 85px 86px
);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: linear-gradient(
180deg,
rgba(3, 7, 28, 0.2) 0%,
rgba(9, 18, 54, 0.07) 48%,
rgba(216, 230, 244, 0.08) 100%
);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
button,
@@ -122,8 +124,8 @@ a {
border-radius: var(--radius-panel);
background: var(--surface-card);
box-shadow: var(--shadow-soft);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(16px) saturate(134%);
backdrop-filter: blur(16px) saturate(134%);
}
.app-shell {
@@ -153,8 +155,8 @@ a {
border-radius: var(--radius-panel);
background: var(--surface-sidebar);
box-shadow: var(--shadow-panel);
-webkit-backdrop-filter: blur(24px);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(18px) saturate(132%);
backdrop-filter: blur(18px) saturate(132%);
transition: width 180ms ease;
}
@@ -165,7 +167,7 @@ a {
align-items: center;
gap: 10px;
padding: 0 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.46);
border-bottom: 1px solid rgba(255, 255, 255, 0.26);
}
.sidebar__brand {
@@ -322,19 +324,21 @@ a {
.clock-greeting__time {
min-height: 1.2em;
margin: 0;
color: var(--text-primary);
color: rgba(248, 250, 252, 0.98);
font-size: clamp(44px, 4.2vw, 56px);
font-weight: 700;
line-height: 1.16;
letter-spacing: -0.035em;
text-shadow: 0 3px 24px rgba(2, 6, 23, 0.48);
text-wrap: balance;
}
.clock-greeting__date {
margin: 12px 0 0;
color: var(--text-secondary);
color: rgba(241, 245, 249, 0.86);
font-size: 16px;
line-height: 1.6;
text-shadow: 0 2px 16px rgba(2, 6, 23, 0.42);
}
.search-box {
@@ -352,8 +356,8 @@ a {
border-radius: var(--radius-panel);
background: var(--surface-search);
box-shadow: var(--shadow-medium);
-webkit-backdrop-filter: blur(22px);
backdrop-filter: blur(22px);
-webkit-backdrop-filter: blur(18px) saturate(136%);
backdrop-filter: blur(18px) saturate(136%);
transition:
border-color 180ms ease,
box-shadow 180ms ease,
@@ -361,23 +365,35 @@ a {
}
.search-box__form:focus-within {
border-color: rgba(59, 130, 246, 0.26);
background: rgba(255, 255, 255, 0.8);
border-color: rgba(191, 219, 254, 0.46);
background: linear-gradient(
135deg,
rgba(245, 249, 253, 0.66),
rgba(218, 231, 245, 0.52)
);
box-shadow:
0 0 0 4px rgba(59, 130, 246, 0.09),
0 0 0 4px rgba(147, 197, 253, 0.12),
var(--shadow-medium);
}
.search-provider-menu__trigger {
min-width: 126px;
width: 48px;
min-width: 48px;
height: 48px;
justify-content: flex-start;
justify-content: center;
margin-left: 10px;
padding-inline: 14px;
padding: 0;
color: #334155;
border-radius: 16px;
}
.search-provider-menu__icon {
width: 24px;
height: 24px;
flex: none;
object-fit: contain;
}
.search-provider-menu__trigger:hover,
.search-provider-menu__trigger[aria-expanded="true"] {
color: var(--primary-hover);
@@ -389,7 +405,7 @@ a {
height: 20px;
flex: none;
margin-left: 10px;
color: var(--text-muted);
color: rgba(51, 65, 85, 0.58);
}
.search-box__input {
@@ -406,7 +422,7 @@ a {
}
.search-box__input::placeholder {
color: var(--text-muted);
color: rgba(51, 65, 85, 0.62);
opacity: 1;
}
@@ -509,9 +525,10 @@ a {
.quick-links__title {
margin: 0 0 18px;
color: var(--text-primary);
color: rgba(248, 250, 252, 0.96);
font-size: 16px;
font-weight: 650;
text-shadow: 0 2px 14px rgba(2, 6, 23, 0.44);
}
.quick-links__grid {
@@ -533,8 +550,8 @@ a {
border-radius: var(--radius-card);
background: var(--surface-card);
box-shadow: var(--shadow-soft);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(16px) saturate(134%);
backdrop-filter: blur(16px) saturate(134%);
transition:
transform 180ms ease,
background 180ms ease,
@@ -544,9 +561,9 @@ a {
.quick-link-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.76);
border-color: rgba(255, 255, 255, 0.54);
background: var(--surface-card-hover);
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.14);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.quick-link-card > span:last-child {
@@ -72,6 +72,34 @@ describe("SearchBox", () => {
expect(input).toHaveFocus()
})
it("闭合态只显示当前图标,展开后显示 Provider 名称", async () => {
const user = userEvent.setup()
renderSearchBox()
const trigger = screen.getByRole("button", {
name: "选择搜索方式,当前为 Google",
})
expect(trigger.textContent).toBe("")
expect(trigger.querySelector("img")).toHaveAttribute(
"src",
"/icons/google.svg",
)
await user.click(trigger)
for (const [label, icon] of [
["Google", "google"],
["Bing", "bing"],
["GitHub", "github"],
["本地", "local"],
] as const) {
const item = screen.getByRole("menuitemradio", { name: label })
expect(item.querySelector("img")).toHaveAttribute(
"src",
`/icons/${icon}.svg`,
)
}
})
it("Google 模式回车只执行当前页外部导航", async () => {
const user = userEvent.setup()
const { onExternalNavigate } = renderSearchBox()
@@ -1,12 +1,6 @@
"use client"
import {
Database,
GitFork,
Globe2,
Search,
type LucideIcon,
} from "lucide-react"
import Image from "next/image"
import { Button } from "@/components/ui/button"
import {
@@ -26,11 +20,24 @@ const PROVIDER_LABELS: Record<SearchProvider, string> = {
local: "本地",
}
const PROVIDER_ICONS: Record<SearchProvider, LucideIcon> = {
google: Globe2,
bing: Search,
github: GitFork,
local: Database,
const PROVIDER_ICON_PATHS: Record<SearchProvider, string> = {
google: "/icons/google.svg",
bing: "/icons/bing.svg",
github: "/icons/github.svg",
local: "/icons/local.svg",
}
function ProviderIcon({ provider }: { provider: SearchProvider }) {
return (
<Image
className="search-provider-menu__icon"
src={PROVIDER_ICON_PATHS[provider]}
alt=""
width={24}
height={24}
aria-hidden="true"
/>
)
}
type SearchProviderMenuProps = {
@@ -44,8 +51,6 @@ export function SearchProviderMenu({
enabledProviders,
onProviderChange,
}: SearchProviderMenuProps) {
const CurrentIcon = PROVIDER_ICONS[provider]
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -55,8 +60,7 @@ export function SearchProviderMenu({
className="search-provider-menu__trigger"
aria-label={`选择搜索方式,当前为 ${PROVIDER_LABELS[provider]}`}
>
<CurrentIcon aria-hidden="true" />
<span>{PROVIDER_LABELS[provider]}</span>
<ProviderIcon provider={provider} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
@@ -67,13 +71,12 @@ export function SearchProviderMenu({
}}
>
{enabledProviders.map((enabledProvider) => {
const Icon = PROVIDER_ICONS[enabledProvider]
return (
<DropdownMenuRadioItem
key={enabledProvider}
value={enabledProvider}
>
<Icon aria-hidden="true" />
<ProviderIcon provider={enabledProvider} />
{PROVIDER_LABELS[enabledProvider]}
</DropdownMenuRadioItem>
)