feat: redesign landing hero + live search + dynamic button (GLM 5.2)

- Complete visual overhaul by GLM 5.2: premium design with aurora glow,
  dot-grid background, glassmorphism search shell
- Live search integrated into hero: debounced search of public links
- Dynamic action button: exact match → Go, no match → 创建个人链接
- Login callback flow for unauthenticated users
- Search suggestions chips (canva, piano, claude)
- Full responsive mobile support
This commit is contained in:
Hermes Agent
2026-06-21 09:57:48 +10:00
parent bd31662ab8
commit 777785ea56
2 changed files with 798 additions and 120 deletions
+384 -50
View File
@@ -1,77 +1,411 @@
import { useState } from 'react';
import { ArrowRight, Search } from 'lucide-react';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
} from 'react';
import { ArrowRight, Plus, Search, X } from 'lucide-react';
import { type Link, listPublicLinks } from '../lib/api';
import { safeLinkTargetUrl } from '../lib/url';
import { useCurrentUser } from '../lib/auth';
const DEBOUNCE_MS = 220;
const MAX_RESULTS = 8;
const MIN_QUERY_LENGTH = 1;
const SUGGESTIONS = ['canva', 'piano', 'claude'] as const;
interface SearchState {
readonly links: Link[];
readonly loading: boolean;
readonly error: string | null;
readonly query: string;
}
const INITIAL_STATE: SearchState = { links: [], loading: false, error: null, query: '' };
function isExactMatch(link: Link, query: string): boolean {
return link.alias.toLowerCase() === query.trim().toLowerCase();
}
export default function LandingHero() {
const [query, setQuery] = useState('');
const [searching, setSearching] = useState(false);
const { user } = useCurrentUser();
const [inputValue, setInputValue] = useState('');
const [state, setState] = useState<SearchState>(INITIAL_STATE);
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
const trimmed = query.trim().toLowerCase();
if (!trimmed) return;
setSearching(true);
window.location.href = `/${trimmed}`;
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const requestIdRef = useRef(0);
const trimmedQuery = inputValue.trim();
const fetchResults = useCallback(async (query: string) => {
const requestId = ++requestIdRef.current;
setState((prev) => ({ ...prev, loading: true, error: null, query }));
try {
const result = await listPublicLinks(query);
if (requestId !== requestIdRef.current) return;
const links = (result.links ?? []).slice(0, MAX_RESULTS);
setState({ links, loading: false, error: null, query });
setOpen(true);
setActiveIndex(-1);
} catch (err) {
if (requestId !== requestIdRef.current) return;
setState({
links: [],
loading: false,
error: err instanceof Error ? err.message : '搜索失败',
query,
});
setOpen(true);
setActiveIndex(-1);
}
}, []);
useEffect(() => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
}
if (trimmedQuery.length < MIN_QUERY_LENGTH) {
setState(INITIAL_STATE);
setOpen(false);
setActiveIndex(-1);
return;
}
debounceRef.current = setTimeout(() => {
void fetchResults(trimmedQuery);
}, DEBOUNCE_MS);
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
}
};
}, [trimmedQuery, fetchResults]);
useEffect(() => {
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
requestIdRef.current++;
};
}, []);
useEffect(() => {
function handleClickOutside(event: Event) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, []);
const results = useMemo(() => state.links, [state.links]);
const exactMatch = useMemo(
() => results.find((link) => isExactMatch(link, trimmedQuery)) ?? null,
[results, trimmedQuery],
);
const showDropdown = open && trimmedQuery.length >= MIN_QUERY_LENGTH;
const showLoading = showDropdown && state.loading;
const showError = showDropdown && !!state.error;
const showNoResults = showDropdown && !state.loading && !state.error && results.length === 0;
const showResults = showDropdown && !state.loading && !state.error && results.length > 0;
function goToShortlink(alias: string) {
window.location.href = `/${alias}`;
}
function handleCreate() {
// 未登录时 #/my-links 自动渲染登录页;登录后路由停留在 my-links(即"我的链接/创建链接"页)
// 携带 create 提示参数,供未来扩展自动展开创建表单
const sep = window.location.hash.includes('?') ? '&' : '?';
window.location.hash = `#/my-links${sep}create=1`;
}
function handlePrimaryAction() {
if (exactMatch) {
goToShortlink(exactMatch.alias);
} else if (user) {
// 已登录但当前在公开着陆页 → 直接进入我的链接页
window.location.hash = '#/my-links';
} else if (trimmedQuery) {
handleCreate();
}
}
function handleSelect(link: Link) {
goToShortlink(link.alias);
}
function handleKeyDown(event: ReactKeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
event.preventDefault();
if (showDropdown && activeIndex >= 0 && activeIndex < results.length) {
handleSelect(results[activeIndex]);
} else {
handlePrimaryAction();
}
return;
}
if (!showDropdown || results.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
setOpen(true);
setActiveIndex((prev) => (prev + 1) % results.length);
break;
case 'ArrowUp':
event.preventDefault();
setActiveIndex((prev) => (prev <= 0 ? results.length - 1 : prev - 1));
break;
case 'Escape':
event.preventDefault();
setOpen(false);
setActiveIndex(-1);
inputRef.current?.blur();
break;
case 'Tab':
setOpen(false);
break;
}
}
function clearSearch() {
setInputValue('');
setState(INITIAL_STATE);
setOpen(false);
setActiveIndex(-1);
inputRef.current?.focus();
}
function pickSuggestion(alias: string) {
setInputValue(alias);
inputRef.current?.focus();
}
const hasQuery = trimmedQuery.length > 0;
const canCreate = hasQuery && !exactMatch && !state.loading;
const primaryLabel = exactMatch ? 'Go' : canCreate ? '创建个人链接' : 'Go';
const primaryDisabled = !exactMatch && !canCreate;
const primaryVariant = exactMatch ? 'is-go' : canCreate ? 'is-create' : 'is-idle';
return (
<section className="landing-hero">
<section className="landing-hero" ref={containerRef}>
<div className="hero-aurora" aria-hidden="true" />
<div className="hero-grid" aria-hidden="true" />
<div className="hero-content">
<div className="hero-badge"></div>
<div className="hero-eyebrow">
<span className="hero-eyebrow__dot" aria-hidden="true" />
<span></span>
</div>
<h1 className="hero-title">
<span className="hero-brand">Heygo</span>
<span className="hero-suffix"></span>
<span className="hero-suffix"></span>
</h1>
<p className="hero-description">
<br />
<br />
</p>
<div className="hero-features">
<div className="feature-pill">
<span className="feature-dot" />
</div>
<div className="feature-pill">
<span className="feature-dot" />
</div>
<div className="feature-pill">
<span className="feature-dot" />
</div>
<div className="feature-pill">
<span className="feature-dot" />
</div>
<div className="hero-features" aria-hidden="true">
<div className="feature-pill"><span className="feature-dot" /></div>
<div className="feature-pill"><span className="feature-dot" /></div>
<div className="feature-pill"><span className="feature-dot" /></div>
<div className="feature-pill"><span className="feature-dot" /></div>
</div>
</div>
<form className="hero-search-box" onSubmit={handleSearch}>
<div className="search-row">
<div className="search-prefix">
<span className="search-prefix-icon"><Search size={16} /></span>
<span className="search-prefix-text">heygo.cc/</span>
<div className={`hero-search${showDropdown ? ' is-open' : ''}`}>
<div className="hero-search__shell">
<div className="hero-search__prefix">
<Search className="hero-search__prefix-icon" size={16} strokeWidth={2.4} aria-hidden="true" />
<span className="hero-search__prefix-text">heygo.cc/</span>
</div>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="输入短链名称"
className="search-field"
autoFocus
className="hero-search__field"
placeholder="输入短链名称,例如 canva"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={() => {
if (results.length > 0 && trimmedQuery.length >= MIN_QUERY_LENGTH) setOpen(true);
}}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
enterKeyHint="go"
aria-label="搜索短链"
aria-expanded={showDropdown}
aria-autocomplete="list"
aria-controls="hero-search-results"
role="combobox"
/>
{hasQuery ? (
<button
type="button"
className="hero-search__clear"
onClick={clearSearch}
aria-label="清除搜索"
tabIndex={-1}
>
<X size={16} strokeWidth={2.5} aria-hidden="true" />
</button>
) : null}
<button
type="submit"
className="search-go"
disabled={searching || !query.trim()}
type="button"
className={`hero-search__action ${primaryVariant}`}
onClick={handlePrimaryAction}
disabled={primaryDisabled}
aria-label={exactMatch ? `跳转到 /${exactMatch.alias}` : primaryLabel}
>
<ArrowRight size={18} />
{exactMatch ? (
<>
<span className="hero-search__action-label">{primaryLabel}</span>
<ArrowRight size={18} strokeWidth={2.5} aria-hidden="true" />
</>
) : canCreate ? (
<>
<Plus size={16} strokeWidth={2.6} aria-hidden="true" />
<span className="hero-search__action-label">{primaryLabel}</span>
</>
) : (
<>
<span className="hero-search__action-label">{primaryLabel}</span>
<ArrowRight size={18} strokeWidth={2.5} aria-hidden="true" />
</>
)}
</button>
</div>
<p className="search-hint">
canvapianoclaude
</p>
</form>
{showDropdown ? (
<div id="hero-search-results" className="hero-search__dropdown" role="listbox">
{showLoading ? (
<div className="hero-search__status">
<span className="hero-search__spinner" aria-hidden="true" />
<span></span>
</div>
) : null}
{showError ? (
<div className="hero-search__status hero-search__status--error" role="alert">
{state.error}
</div>
) : null}
{showNoResults ? (
<div className="hero-search__empty">
<div className="hero-search__empty-title">
<code>{trimmedQuery}</code>
</div>
<div className="hero-search__empty-hint">
</div>
<button
type="button"
className="hero-search__empty-cta"
onClick={handleCreate}
>
<Plus size={15} strokeWidth={2.6} aria-hidden="true" />
</button>
</div>
) : null}
{showResults
? results.map((link, index) => {
const exact = isExactMatch(link, trimmedQuery);
const isActive = index === activeIndex;
const safeUrl =
link.linkType === 'redirect' ? safeLinkTargetUrl(link.targetUrl) : null;
return (
<div
key={link.id}
role="option"
aria-selected={isActive}
className={`hero-search__result${isActive ? ' is-active' : ''}${
exact ? ' is-exact' : ''
}`}
onClick={() => handleSelect(link)}
onMouseEnter={() => setActiveIndex(index)}
>
<div className="hero-search__result-main">
<span className="hero-search__result-alias">
<span className="hero-search__result-slash">/</span>
{link.alias}
</span>
{exact ? (
<span className="hero-search__exact-badge"></span>
) : null}
</div>
{link.linkType === 'redirect' && link.targetUrl ? (
<p className="hero-search__result-url">
{safeUrl ? (
<span className="truncate" title={link.targetUrl}>
{link.targetUrl}
</span>
) : (
<span className="muted"></span>
)}
</p>
) : link.linkType === 'custom' ? (
<p className="hero-search__result-url">
<span className="muted"></span>
</p>
) : null}
<div className="hero-search__result-meta">
<span className="hero-search__clicks">
{link.clickCount.toLocaleString()}
</span>
{link.description ? (
<span className="hero-search__result-desc">{link.description}</span>
) : null}
</div>
</div>
);
})
: null}
</div>
) : null}
</div>
<div className="hero-suggestions">
<span className="hero-suggestions__label"></span>
{SUGGESTIONS.map((alias) => (
<button
key={alias}
type="button"
className="hero-suggestion-chip"
onClick={() => pickSuggestion(alias)}
>
{alias}
</button>
))}
</div>
</section>
);
}
+414 -70
View File
@@ -1975,68 +1975,130 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
display: flex;
flex-direction: column;
align-items: center;
gap: 2.5rem;
padding: 5rem 0 1rem;
gap: 2.25rem;
padding: clamp(3.5rem, 9vh, 6rem) 1rem 1.5rem;
width: 100%;
position: relative;
isolation: isolate;
overflow: hidden;
}
.landing-hero::before {
content: '';
/* Aurora — soft, off-center accent glow */
.hero-aurora {
position: absolute;
inset: -20% -10% 20% -10%;
z-index: -2;
pointer-events: none;
background:
radial-gradient(48% 38% at 28% 8%, rgba(99, 102, 241, 0.16), transparent 72%),
radial-gradient(40% 32% at 78% 4%, rgba(168, 85, 247, 0.10), transparent 70%),
radial-gradient(60% 40% at 50% 100%, rgba(99, 102, 241, 0.06), transparent 75%);
filter: blur(8px);
animation: hero-aurora-drift 18s ease-in-out infinite alternate;
}
@keyframes hero-aurora-drift {
0% { transform: translate3d(-1.5%, 0, 0) scale(1); }
100% { transform: translate3d(2%, -1%, 0) scale(1.04); }
}
/* Faint dot-grid for texture, masked toward the center */
.hero-grid {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 60% 50% at 50% 0%, rgba(99,102,241,0.06) 0%, transparent 70%);
z-index: -1;
pointer-events: none;
background-image: radial-gradient(rgba(15, 23, 42, 0.05) 1px, transparent 1px);
background-size: 22px 22px;
background-position: center;
-webkit-mask-image: radial-gradient(ellipse 55% 45% at 50% 32%, #000 0%, transparent 78%);
mask-image: radial-gradient(ellipse 55% 45% at 50% 32%, #000 0%, transparent 78%);
opacity: 0.55;
}
.hero-content {
text-align: center;
max-width: 600px;
max-width: 620px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.hero-badge {
.hero-eyebrow {
display: inline-flex;
background: linear-gradient(135deg, rgba(99,102,241,0.12), rgba(129,140,248,0.08));
border: 1px solid rgba(99,102,241,0.18);
align-items: center;
gap: 0.5rem;
background: color-mix(in srgb, var(--surface) 92%, transparent);
border: 1px solid rgba(99, 102, 241, 0.16);
border-radius: 999px;
padding: 0.35rem 1rem;
padding: 0.4rem 0.95rem 0.4rem 0.7rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--accent-text);
margin-bottom: 1.25rem;
letter-spacing: 0.02em;
margin-bottom: 1.5rem;
letter-spacing: 0.005em;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 6px 18px rgba(99, 102, 241, 0.06);
backdrop-filter: blur(6px);
}
.hero-eyebrow__dot {
position: relative;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.55);
animation: hero-pulse 2.4s ease-out infinite;
}
@keyframes hero-pulse {
0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.5); }
70% { box-shadow: 0 0 0 8px rgba(99, 102, 241, 0); }
100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
}
.hero-title {
display: flex;
align-items: center;
align-items: baseline;
justify-content: center;
gap: 0.5rem;
margin: 0 0 0.75rem;
gap: 0.65rem;
margin: 0 0 1rem;
flex-wrap: wrap;
}
.hero-brand {
font-family: 'Syne', ui-sans-serif, sans-serif;
font-size: 4rem;
font-size: clamp(2.75rem, 7vw, 4.25rem);
font-weight: 800;
letter-spacing: -0.04em;
letter-spacing: -0.045em;
color: var(--text);
line-height: 1;
line-height: 0.95;
background: linear-gradient(180deg, var(--text) 0%, #1E293B 100%);
-webkit-background-clip: text;
background-clip: text;
}
.hero-suffix {
font-size: 1.75rem;
font-weight: 700;
color: var(--accent);
font-family: 'Syne', ui-sans-serif, sans-serif;
font-size: clamp(1.2rem, 3vw, 1.85rem);
font-weight: 800;
letter-spacing: -0.02em;
color: transparent;
background: linear-gradient(120deg, var(--accent) 0%, #A855F7 100%);
-webkit-background-clip: text;
background-clip: text;
line-height: 1;
position: relative;
top: -0.06em;
}
.hero-description {
font-size: 1.0625rem;
font-size: clamp(0.9375rem, 2.2vw, 1.0625rem);
line-height: 1.7;
margin: 0 auto 1.75rem;
max-width: 460px;
max-width: 480px;
color: var(--muted);
}
@@ -2048,23 +2110,25 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
}
.feature-pill {
background: var(--surface);
background: color-mix(in srgb, var(--surface) 88%, transparent);
border: 1px solid var(--border);
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 0.4rem;
gap: 0.42rem;
font-size: 0.8125rem;
font-weight: 500;
padding: 0.35rem 0.85rem;
padding: 0.34rem 0.85rem;
color: var(--text);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(15,23,42,0.04);
transition: border-color 0.15s, box-shadow 0.15s;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition: border-color 0.18s, box-shadow 0.18s, transform 0.18s;
backdrop-filter: blur(4px);
}
.feature-pill:hover {
border-color: var(--accent);
border-color: rgba(99, 102, 241, 0.4);
box-shadow: 0 0 0 3px var(--accent-soft);
transform: translateY(-1px);
}
.feature-dot {
@@ -2073,57 +2137,80 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
box-shadow: 0 0 6px rgba(99, 102, 241, 0.6);
}
.hero-search-box {
/* ── Search — the visual focal point ── */
.hero-search {
width: 100%;
max-width: 540px;
max-width: 580px;
position: relative;
display: flex;
flex-direction: column;
}
.search-row {
.hero-search__shell {
display: flex;
align-items: stretch;
border: 2px solid var(--border-strong);
border-radius: 12px;
height: 60px;
border: 1.5px solid var(--border-strong);
border-radius: 16px;
overflow: hidden;
background: var(--surface);
transition: border-color 0.2s, box-shadow 0.2s;
background: color-mix(in srgb, var(--surface) 92%, transparent);
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.04),
0 12px 32px -10px rgba(15, 23, 42, 0.14),
0 4px 12px -6px rgba(99, 102, 241, 0.16);
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
backdrop-filter: blur(10px);
}
.search-row:focus-within {
.hero-search.is-open .hero-search__shell {
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
.hero-search__shell:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--accent-soft), 0 4px 20px rgba(99,102,241,0.08);
box-shadow:
0 0 0 4px var(--accent-soft),
0 14px 36px -10px rgba(99, 102, 241, 0.28),
0 6px 16px -6px rgba(99, 102, 241, 0.22);
transform: translateY(-1px);
}
.search-prefix {
.hero-search__prefix {
display: flex;
align-items: center;
gap: 0.4rem;
background: var(--background);
border-right: 1px solid var(--border);
padding: 0 0.85rem;
gap: 0.45rem;
padding: 0 0.4rem 0 1.1rem;
flex-shrink: 0;
color: var(--muted);
}
.search-prefix-icon {
.hero-search__prefix-icon {
color: var(--muted);
display: flex;
opacity: 0.85;
}
.search-prefix-text {
.hero-search__prefix-text {
color: var(--accent-text);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.875rem;
font-size: 0.9375rem;
font-weight: 600;
white-space: nowrap;
opacity: 0.85;
}
.search-field {
.hero-search__field {
border: none;
font: inherit;
font-size: 1rem;
padding: 0.85rem 0.85rem;
font-size: 1.0625rem;
font-weight: 450;
letter-spacing: -0.01em;
padding: 0 0.6rem;
flex: 1;
min-width: 0;
outline: none;
@@ -2131,28 +2218,279 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
color: var(--text);
}
.search-field::placeholder {
.hero-search__field::placeholder {
color: var(--muted);
opacity: 0.6;
opacity: 0.55;
}
.search-go {
.hero-search__clear {
background: transparent;
border: none;
border-left: 1px solid var(--border);
color: var(--accent);
color: var(--muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1.1rem;
transition: background 0.15s, color 0.15s;
padding: 0 0.6rem;
flex-shrink: 0;
border-radius: 8px;
margin: auto 0.35rem;
height: 32px;
width: 32px;
transition: background 0.15s, color 0.15s;
}
.hero-search__clear:hover {
background: var(--background);
color: var(--text);
}
.search-go:hover { background: var(--accent); color: white; }
.search-go:disabled { color: var(--muted); cursor: not-allowed; }
.search-go:disabled:hover { background: transparent; color: var(--muted); }
/* Dynamic action button — changes label/role based on match state */
.hero-search__action {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.42rem;
border: none;
border-left: 1px solid var(--border);
cursor: pointer;
padding: 0 1.4rem;
margin: 0;
flex-shrink: 0;
font-weight: 650;
font-size: 0.9375rem;
letter-spacing: -0.01em;
white-space: nowrap;
transition: background 0.18s, color 0.18s, box-shadow 0.18s, transform 0.18s;
-webkit-tap-highlight-color: transparent;
}
.hero-search__action-label { line-height: 1; }
/* Idle (no query) — subtle, disabled-looking */
.hero-search__action.is-idle {
background: transparent;
color: var(--muted);
cursor: default;
}
/* Exact match → "Go": solid accent, punchy */
.hero-search__action.is-go {
background: var(--accent);
color: #fff;
}
.hero-search__action.is-go:hover {
background: var(--accent-hover);
box-shadow: 0 8px 20px -6px rgba(99, 102, 241, 0.5);
transform: translateY(-1px);
}
/* No match → "创建个人链接": dark, premium CTA */
.hero-search__action.is-create {
background: var(--text);
color: #fff;
}
.hero-search__action.is-create:hover {
background: #1E293B;
box-shadow: 0 8px 20px -6px rgba(15, 23, 42, 0.4);
transform: translateY(-1px);
}
.hero-search__action:disabled {
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.hero-search__action.is-go:disabled,
.hero-search__action.is-create:disabled {
opacity: 0.5;
}
/* ── Dropdown results ── */
.hero-search__dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
z-index: 30;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow:
0 18px 48px -12px rgba(15, 23, 42, 0.22),
0 6px 16px -8px rgba(15, 23, 42, 0.1);
overflow: hidden;
max-height: 60vh;
overflow-y: auto;
animation: hero-dropdown-in 0.16s ease-out;
}
@keyframes hero-dropdown-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-search__status {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.95rem 1rem;
color: var(--muted);
font-size: 0.875rem;
}
.hero-search__status--error { color: var(--danger); }
.hero-search__spinner {
width: 14px;
height: 14px;
border: 2px solid var(--border-strong);
border-top-color: var(--accent);
border-radius: 50%;
animation: hero-spinner 0.7s linear infinite;
}
@keyframes hero-spinner { to { transform: rotate(360deg); } }
.hero-search__empty {
padding: 1.1rem 1rem 1.2rem;
text-align: center;
}
.hero-search__empty-title {
font-size: 0.9375rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.3rem;
}
.hero-search__empty-title code { margin: 0 0.15em; }
.hero-search__empty-hint {
font-size: 0.8125rem;
color: var(--muted);
margin-bottom: 0.85rem;
}
.hero-search__empty-cta {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--text);
color: #fff;
border: none;
border-radius: 999px;
padding: 0.5rem 1rem;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, transform 0.15s, box-shadow 0.15s;
}
.hero-search__empty-cta:hover {
background: #1E293B;
transform: translateY(-1px);
box-shadow: 0 6px 16px -6px rgba(15, 23, 42, 0.4);
}
.hero-search__result {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.7rem 1rem;
cursor: pointer;
border-bottom: 1px solid var(--border);
transition: background 0.12s;
}
.hero-search__result:last-child { border-bottom: none; }
.hero-search__result.is-active { background: var(--accent-soft); }
.hero-search__result.is-exact { box-shadow: inset 3px 0 0 var(--accent); }
.hero-search__result-main {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.hero-search__result-alias {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.9375rem;
font-weight: 600;
color: var(--accent-text);
}
.hero-search__result-slash { opacity: 0.5; margin-right: 0.05em; }
.hero-search__exact-badge {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--accent);
background: var(--accent-soft);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 999px;
padding: 0.1rem 0.5rem;
text-transform: uppercase;
}
.hero-search__result-url {
margin: 0;
font-size: 0.8125rem;
color: var(--muted);
line-height: 1.4;
}
.hero-search__result-url .truncate {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.hero-search__result-meta {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.75rem;
color: var(--muted);
}
.hero-search__result-desc {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 24rem;
opacity: 0.85;
}
/* ── Suggestion chips ── */
.hero-suggestions {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
justify-content: center;
max-width: 580px;
}
.hero-suggestions__label {
font-size: 0.8125rem;
color: var(--muted);
opacity: 0.7;
}
.hero-suggestion-chip {
background: color-mix(in srgb, var(--surface) 80%, transparent);
border: 1px solid var(--border);
border-radius: 999px;
padding: 0.3rem 0.8rem;
font-size: 0.8125rem;
font-weight: 500;
color: var(--text);
cursor: pointer;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
transition: border-color 0.15s, background 0.15s, transform 0.15s, box-shadow 0.15s;
}
.hero-suggestion-chip:hover {
border-color: rgba(99, 102, 241, 0.45);
background: var(--accent-soft);
color: var(--accent-text);
transform: translateY(-1px);
box-shadow: 0 4px 12px -4px rgba(99, 102, 241, 0.3);
}
.search-hint {
color: var(--muted);
@@ -2217,17 +2555,23 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
}
/* Landing Hero — 移动端适配 */
.landing-hero { padding: 3rem 0 0.5rem; gap: 2rem; }
.hero-brand { font-size: 2.5rem; }
.hero-suffix { font-size: 1.2rem; }
.landing-hero { padding: clamp(2.5rem, 8vh, 3.5rem) 1rem 1rem; gap: 1.75rem; }
.hero-brand { font-size: clamp(2.25rem, 11vw, 2.75rem); }
.hero-suffix { font-size: 1.15rem; }
.hero-description { font-size: 0.9375rem; }
.hero-badge { font-size: 0.75rem; }
.feature-pill { font-size: 0.75rem; padding: 0.25rem 0.65rem; }
.search-row { border-radius: 10px; }
.search-field { font-size: 0.9375rem; padding: 0.75rem 0.65rem; }
.search-prefix { padding: 0 0.65rem; }
.search-prefix-text { font-size: 0.8rem; }
.search-go { padding: 0 0.85rem; }
.search-hint { font-size: 0.75rem; }
.hero-eyebrow { font-size: 0.75rem; padding: 0.35rem 0.85rem 0.35rem 0.6rem; }
.feature-pill { font-size: 0.75rem; padding: 0.25rem 0.7rem; }
.hero-search__shell { height: 54px; border-radius: 14px; }
.hero-search.is-open .hero-search__shell { border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; }
.hero-search__field { font-size: 0.9375rem; }
.hero-search__prefix { padding: 0 0.3rem 0 0.85rem; }
.hero-search__prefix-text { font-size: 0.8rem; }
.hero-search__action { padding: 0 1rem; font-size: 0.875rem; }
.hero-search__action-label { display: none; }
.hero-search__action.is-create .hero-search__action-label { display: none; }
.hero-search__action.is-go .hero-search__action-label { display: none; }
.hero-search__dropdown { max-height: 52vh; border-radius: 12px; }
.hero-suggestions { gap: 0.4rem; }
.hero-suggestion-chip { font-size: 0.75rem; padding: 0.28rem 0.7rem; }
.hero-content { max-width: 100%; }
}