diff --git a/ts/web/nextjs/polaris/next-env.d.ts b/ts/web/nextjs/polaris/next-env.d.ts index ce4e94a..a419cbe 100644 --- a/ts/web/nextjs/polaris/next-env.d.ts +++ b/ts/web/nextjs/polaris/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/types/routes.d.ts"; -import "./.next/types/root-params.d.ts"; +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/ts/web/nextjs/polaris/public/backgrounds/test.jpg b/ts/web/nextjs/polaris/public/backgrounds/test.jpg new file mode 100644 index 0000000..61ecead Binary files /dev/null and b/ts/web/nextjs/polaris/public/backgrounds/test.jpg differ diff --git a/ts/web/nextjs/polaris/public/icons/bing.svg b/ts/web/nextjs/polaris/public/icons/bing.svg new file mode 100644 index 0000000..762165a --- /dev/null +++ b/ts/web/nextjs/polaris/public/icons/bing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ts/web/nextjs/polaris/public/icons/github.svg b/ts/web/nextjs/polaris/public/icons/github.svg new file mode 100644 index 0000000..e1aa6fc --- /dev/null +++ b/ts/web/nextjs/polaris/public/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ts/web/nextjs/polaris/public/icons/google.svg b/ts/web/nextjs/polaris/public/icons/google.svg new file mode 100644 index 0000000..3cca8a8 --- /dev/null +++ b/ts/web/nextjs/polaris/public/icons/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ts/web/nextjs/polaris/public/icons/local.svg b/ts/web/nextjs/polaris/public/icons/local.svg new file mode 100644 index 0000000..ddf16c4 --- /dev/null +++ b/ts/web/nextjs/polaris/public/icons/local.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ts/web/nextjs/polaris/src/app/globals.css b/ts/web/nextjs/polaris/src/app/globals.css index 495ccef..c8eacca 100644 --- a/ts/web/nextjs/polaris/src/app/globals.css +++ b/ts/web/nextjs/polaris/src/app/globals.css @@ -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 { diff --git a/ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx b/ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx index b88324a..08dccc1 100644 --- a/ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx +++ b/ts/web/nextjs/polaris/src/features/home/components/search-box.test.tsx @@ -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() diff --git a/ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx b/ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx index 7f9f1f3..1129903 100644 --- a/ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx +++ b/ts/web/nextjs/polaris/src/features/home/components/search-provider-menu.tsx @@ -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 = { local: "本地", } -const PROVIDER_ICONS: Record = { - google: Globe2, - bing: Search, - github: GitFork, - local: Database, +const PROVIDER_ICON_PATHS: Record = { + google: "/icons/google.svg", + bing: "/icons/bing.svg", + github: "/icons/github.svg", + local: "/icons/local.svg", +} + +function ProviderIcon({ provider }: { provider: SearchProvider }) { + return ( + + ) } type SearchProviderMenuProps = { @@ -44,8 +51,6 @@ export function SearchProviderMenu({ enabledProviders, onProviderChange, }: SearchProviderMenuProps) { - const CurrentIcon = PROVIDER_ICONS[provider] - return ( @@ -55,8 +60,7 @@ export function SearchProviderMenu({ className="search-provider-menu__trigger" aria-label={`选择搜索方式,当前为 ${PROVIDER_LABELS[provider]}`} > - @@ -67,13 +71,12 @@ export function SearchProviderMenu({ }} > {enabledProviders.map((enabledProvider) => { - const Icon = PROVIDER_ICONS[enabledProvider] return ( - )