1use dioxus::prelude::*;
4
5use crate::components::admin_layout::{AdminShell, AdminSidebar};
6use crate::components::footer::FooterView;
7use crate::components::frontend_layout::FrontendShell;
8use crate::components::header::{Header, SearchIconLink};
9use crate::components::nav::build_nav_items;
10use crate::components::post::breadcrumbs::Breadcrumbs;
11use crate::components::post::post_content::PostContent;
12use crate::components::post::post_footer::PostFooter;
13use crate::components::post::post_header::PostHeader;
14use crate::components::post::post_meta::PostMeta;
15use crate::components::post::post_nav_links::PostNavLinks;
16use crate::components::post::post_toc::PostToc;
17use crate::components::skeletons::home_skeleton::HomeSkeleton;
18use crate::models::post::{PostNav, PostStatus};
19use crate::router::Route;
20use crate::theme::ThemeToggle;
21
22pub(super) fn preview(slug: &str, detail: bool) -> Option<Element> {
23 match slug {
24 "admin-layout" => Some(rsx! { AdminLayoutPreview { detail } }),
25 "header" => Some(rsx! { HeaderPreview { detail } }),
26 "search-icon-link" => Some(rsx! { SearchIconLinkPreview { detail } }),
27 "footer" => Some(rsx! { FooterPreview { detail } }),
28 "frontend-layout" => Some(rsx! { FrontendLayoutPreview { detail } }),
29 "breadcrumbs" => Some(rsx! { BreadcrumbsPreview { detail } }),
30 "post-header" => Some(rsx! { PostHeaderPreview { detail } }),
31 "post-meta" => Some(rsx! { PostMetaPreview {} }),
32 "post-content" => Some(rsx! { PostContentPreview { detail } }),
33 "post-footer" => Some(rsx! { PostFooterPreview { detail } }),
34 "post-nav-links" => Some(rsx! { PostNavLinksPreview { detail } }),
35 "post-toc" => Some(rsx! { PostTocPreview { detail } }),
36 _ => None,
37 }
38}
39
40#[component]
41fn PostMetaPreview() -> Element {
42 let post = super::sample_post();
43
44 rsx! {
45 article { class: "showcase-post-meta-demo", "data-showcase-preview": "post-meta",
46 p { class: "showcase-post-meta-kicker", "设计手记" }
47 h3 { class: "post-title", "{post.title}" }
48 p { class: "post-description", "{post.summary.as_deref().unwrap_or_default()}" }
49 PostMeta { post }
50 }
51 }
52}
53
54#[component]
55fn PostHeaderPreview(detail: bool) -> Element {
56 let mut post = super::sample_post();
57 post.summary = Some(
58 "在时间的缝隙里,种下一些文字。记录设计、代码与日常,让零散的念头慢慢长成一片风景。"
59 .to_string(),
60 );
61 let mut draft = post.clone();
62 draft.title = "还在生长的想法".to_string();
63 draft.summary = Some("这一页,留给尚未完成的念头。".to_string());
64 draft.status = PostStatus::Draft;
65 draft.published_at = None;
66 let mut without_summary = post.clone();
67 without_summary.title = "在时间的缝隙里种下一些文字:从零开始构建自己的数字花园".to_string();
68 without_summary.summary = None;
69
70 rsx! {
71 div { class: "showcase-post-header-demo", "data-showcase-preview": "post-header",
72 article { class: "showcase-post-header-sheet", "data-showcase-sample": "published", inert: "true",
73 PostHeader { post }
74 }
75 if detail {
76 div { class: "showcase-post-header-examples",
77 section { class: "showcase-post-header-example",
78 h3 { "草稿" span { "标题旁显示草稿标记" } }
79 article { class: "showcase-post-header-sheet", "data-showcase-sample": "draft", inert: "true",
80 PostHeader { post: draft }
81 }
82 }
83 section { class: "showcase-post-header-example",
84 h3 { "长标题 · 无摘要" span { "自然换行" } }
85 article { class: "showcase-post-header-sheet", "data-showcase-sample": "no-summary", inert: "true",
86 PostHeader { post: without_summary }
87 }
88 }
89 }
90 }
91 }
92 }
93}
94
95#[component]
96fn BreadcrumbsPreview(detail: bool) -> Element {
97 let title = "让想法生根";
98 let long_title = "在时间的缝隙里种下一些文字:从零开始构建 Rust 与 Dioxus 全栈博客的设计与实践";
99
100 rsx! {
101 div { class: "showcase-breadcrumb-demo", "data-showcase-preview": "breadcrumbs",
102 article { class: "showcase-breadcrumb-context",
103 div { class: "showcase-breadcrumb-path", inert: "true",
104 Breadcrumbs { title: title.to_string() }
105 }
106 div { class: "showcase-breadcrumb-copy",
107 p { class: "showcase-breadcrumb-kicker", "随笔 / 设计" }
108 h3 { "{title}" }
109 p { class: "showcase-breadcrumb-summary", "把零散的念头,写成有迹可循的文字。" }
110 }
111 }
112 if detail {
113 div { class: "showcase-breadcrumb-examples",
114 section { class: "showcase-breadcrumb-example",
115 h3 { "长标题" span { "单行省略" } }
116 div { class: "showcase-breadcrumb-path", inert: "true",
117 Breadcrumbs { title: long_title.to_string() }
118 }
119 }
120 section { class: "showcase-breadcrumb-example",
121 h3 { "窄屏" span { "≤ 280px" } }
122 div { class: "showcase-breadcrumb-path showcase-breadcrumb-path--narrow", inert: "true",
123 Breadcrumbs { title: long_title.to_string() }
124 }
125 }
126 }
127 p { class: "showcase-breadcrumb-note", "保留首页入口,当前标题随可用宽度省略。" }
128 }
129 }
130 }
131}
132
133#[component]
134fn AdminLayoutPreview(detail: bool) -> Element {
135 let mut active = use_signal(|| "全部文章");
136 let mut status = use_signal(|| "侧栏操作只更新这份本地演示。");
137
138 let active_name = active();
139 let (stat_1_label, stat_1_val, stat_2_label, stat_2_val) = match active_name {
140 "仪表盘" => ("总访客", "1,280", "今日阅读", "342"),
141 "写文章" => ("当前字数", "1,420", "自动保存", "2分钟前"),
142 "笔记" | "笔记本" => ("公开笔记", "24", "笔记本数", "05"),
143 "回收站" => ("待清理", "02", "保留期限", "30天"),
144 "评论管理" => ("待审核", "01", "累计评论", "156"),
145 "素材" => ("媒体文件", "48", "已占用", "12.8MB"),
146 "友链" => ("正常链接", "18", "待确认", "02"),
147 "个人信息" => ("账号角色", "管理员", "安全状态", "双重认证"),
148 "设置" | "系统" => ("系统状态", "正常", "运行时间", "14天"),
149 "日志" => ("今日日志", "84条", "错误告警", "0"),
150 _ => ("草稿", "03", "已发布", "12"),
151 };
152
153 rsx! {
154 div { class: "showcase-admin-window", "data-showcase-preview": "admin-layout",
155 AdminShell {
156 preview: true,
157 sidebar: rsx! { AdminSidebar {
158 route: Route::Admin {},
159 user_name: Some("图鉴访客".to_string()),
160 avatar_url: None,
161 demo_active: Some(active().to_string()),
162 preview: true,
163 on_demo_navigate: move |label| {
164 active.set(label);
165 status.set("已切换本地示例栏目。");
166 },
167 on_logout: move |_| status.set("这里只演示退出按钮,不会退出当前账号。"),
168 } },
169 content: rsx! {
170 article { class: "showcase-admin-content flex flex-col h-full",
171 div { class: "flex items-center justify-between",
172 p { class: "text-xs font-mono text-paper-secondary tracking-wider", "ADMIN / LOCAL DEMO" }
173 if detail {
174 span { class: "text-xs px-2 py-0.5 rounded-full bg-[var(--color-paper-entry)] text-paper-secondary border border-[var(--color-paper-border)]", "本地演示" }
175 }
176 }
177 h2 { class: "text-xl font-semibold mt-1.5 flex items-center gap-2", "{active()}" }
178 p { class: "text-sm text-paper-secondary mt-1", "{status()}" }
179
180 div { class: "mt-4 grid grid-cols-2 gap-3",
181 div { class: "rounded-2xl bg-paper-entry p-3.5 border border-[var(--color-paper-border)]/50",
182 small { class: "text-paper-secondary text-xs", "{stat_1_label}" }
183 strong { class: "block text-lg mt-0.5 font-bold tracking-tight", "{stat_1_val}" }
184 }
185 div { class: "rounded-2xl bg-paper-entry p-3.5 border border-[var(--color-paper-border)]/50",
186 small { class: "text-paper-secondary text-xs", "{stat_2_label}" }
187 strong { class: "block text-lg mt-0.5 font-bold tracking-tight", "{stat_2_val}" }
188 }
189 }
190
191 if !detail {
192 div { class: "mt-2.5 rounded-lg bg-paper-entry/50 border border-[var(--color-paper-border)]/40 px-2.5 py-1.5 flex items-center justify-between text-xs",
193 div { class: "flex items-center gap-1.5 min-w-0",
194 span { class: "w-1.5 h-1.5 rounded-full bg-emerald-500 flex-shrink-0" }
195 span { class: "truncate font-medium text-[11px]", "响应式设计实践" }
196 }
197 span { class: "text-paper-secondary text-[10px] font-mono flex-shrink-0 ml-1", "已发布" }
198 }
199 }
200
201 if detail {
202 div { class: "mt-5 flex-1 min-h-0 flex flex-col",
203 p { class: "text-xs font-medium text-paper-secondary mb-2 tracking-wide", "示例内容列表" }
204 div { class: "rounded-xl border border-[var(--color-paper-border)] divide-y divide-[var(--color-paper-border)] bg-paper-entry/40 overflow-hidden text-xs",
205 div { class: "px-3.5 py-2.5 flex items-center justify-between hover:bg-paper-entry/80 transition-colors",
206 div { class: "flex items-center gap-2 min-w-0",
207 span { class: "w-1.5 h-1.5 rounded-full bg-emerald-500 flex-shrink-0" }
208 span { class: "truncate font-medium", "Dioxus 0.7 响应式设计实践" }
209 }
210 span { class: "text-paper-secondary flex-shrink-0 ml-2 font-mono text-[11px]", "09-22" }
211 }
212 div { class: "px-3.5 py-2.5 flex items-center justify-between hover:bg-paper-entry/80 transition-colors",
213 div { class: "flex items-center gap-2 min-w-0",
214 span { class: "w-1.5 h-1.5 rounded-full bg-amber-500 flex-shrink-0" }
215 span { class: "truncate font-medium", "Rust 全栈博客架构与布局演进" }
216 }
217 span { class: "text-paper-secondary flex-shrink-0 ml-2 font-mono text-[11px]", "09-18" }
218 }
219 div { class: "px-3.5 py-2.5 flex items-center justify-between hover:bg-paper-entry/80 transition-colors",
220 div { class: "flex items-center gap-2 min-w-0",
221 span { class: "w-1.5 h-1.5 rounded-full bg-emerald-500 flex-shrink-0" }
222 span { class: "truncate font-medium", "Tailwind CSS v4 样式系统接入记录" }
223 }
224 span { class: "text-paper-secondary flex-shrink-0 ml-2 font-mono text-[11px]", "09-15" }
225 }
226 }
227 p { class: "text-xs text-paper-secondary mt-auto pt-3", "可展开内容管理或工具菜单,点击任意栏目查看联动状态。" }
228 }
229 }
230 }
231 },
232 }
233 }
234 }
235}
236
237fn sample_route(label: &str) -> Route {
238 match label {
239 "笔记" => Route::Notes {},
240 "归档" => Route::Archives {},
241 "友链" => Route::Friends {},
242 "关于" => Route::About {},
243 "搜索" => Route::Search {},
244 _ => Route::Home {},
245 }
246}
247
248#[derive(Clone, Copy, PartialEq, Eq)]
249enum HeaderViewportMode {
250 Desktop,
251 Mobile,
252}
253
254#[component]
255fn HeaderPreview(detail: bool) -> Element {
256 let mut active = use_signal(|| "笔记");
257 let mut viewport_mode = use_signal(|| HeaderViewportMode::Desktop);
258 let mut max_width_choice = use_signal(|| "max-w-6xl");
259 let menu_id = if detail {
260 "mobile-nav-menu-showcase-header-detail"
261 } else {
262 "mobile-nav-menu-showcase-header-card"
263 };
264
265 let search_button = rsx! {
266 button {
267 class: if active() == "搜索" {
268 "relative p-2 rounded-full text-paper-accent transition-colors duration-200"
269 } else {
270 "relative p-2 rounded-full text-paper-secondary hover:text-paper-accent transition-colors duration-200"
271 },
272 r#type: "button",
273 aria_label: "演示搜索操作",
274 title: "搜索",
275 onclick: move |_| active.set("搜索"),
276 svg {
277 xmlns: "http://www.w3.org/2000/svg",
278 class: "w-4 h-4 md:w-5 md:h-5",
279 view_box: "0 -960 960 960",
280 fill: "currentColor",
281 path { d: "M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z" }
282 }
283 if active() == "搜索" {
284 span { class: "nav-indicator hidden md:block", "aria-hidden": "true" }
285 }
286 }
287 };
288
289 if !detail {
290 return rsx! {
291 div { class: "showcase-layout-window", "data-showcase-preview": "header",
292 Header {
293 nav_items: build_nav_items(sample_route(active())),
294 right_content: rsx! { {search_button} },
295 max_width: "max-w-6xl",
296 menu_id,
297 page_shell: false,
298 on_demo_navigate: move |label| active.set(label),
299 }
300 div { class: "showcase-header-card-body",
301 div { class: "showcase-header-card-hero",
302 div {
303 p { class: "text-[11px] font-mono text-paper-secondary", "ACTIVE / 当前栏目" }
304 p { class: "text-sm font-semibold text-paper-primary mt-0.5", "{active()}" }
305 }
306 span { class: "text-[10px] px-2 py-0.5 rounded-full bg-paper-accent-soft text-paper-accent font-medium", "交互演示" }
307 }
308 div { class: "showcase-header-card-tags",
309 span { "自适应居中" }
310 span { "平滑下划指示" }
311 span { "窄屏自动折叠" }
312 }
313 }
314 }
315 };
316 }
317
318 rsx! {
319 div { class: "w-full flex flex-col gap-3",
320 div { class: "flex flex-wrap items-center justify-between gap-3 p-3 rounded-xl bg-paper-entry/50 border border-[var(--sc-line)]",
321 div { class: "flex items-center gap-2",
322 span { class: "text-xs font-mono text-paper-secondary tracking-wider", "VIEWPORT" }
323 div { class: "showcase-picker-mode-switch",
324 button {
325 r#type: "button",
326 aria_pressed: "{viewport_mode() == HeaderViewportMode::Desktop}",
327 onclick: move |_| viewport_mode.set(HeaderViewportMode::Desktop),
328 "🖥️ 桌面端"
329 }
330 button {
331 r#type: "button",
332 aria_pressed: "{viewport_mode() == HeaderViewportMode::Mobile}",
333 onclick: move |_| viewport_mode.set(HeaderViewportMode::Mobile),
334 "📱 移动端 (380px)"
335 }
336 }
337 }
338 if viewport_mode() == HeaderViewportMode::Desktop {
339 div { class: "flex items-center gap-2",
340 span { class: "text-xs font-mono text-paper-secondary tracking-wider", "WIDTH" }
341 div { class: "showcase-picker-mode-switch",
342 button {
343 r#type: "button",
344 aria_pressed: "{max_width_choice() == \"max-w-3xl\"}",
345 onclick: move |_| max_width_choice.set("max-w-3xl"),
346 "3xl (768px)"
347 }
348 button {
349 r#type: "button",
350 aria_pressed: "{max_width_choice() == \"max-w-4xl\"}",
351 onclick: move |_| max_width_choice.set("max-w-4xl"),
352 "4xl (896px)"
353 }
354 button {
355 r#type: "button",
356 aria_pressed: "{max_width_choice() == \"max-w-6xl\"}",
357 onclick: move |_| max_width_choice.set("max-w-6xl"),
358 "6xl (1152px)"
359 }
360 }
361 }
362 }
363 }
364
365 div {
366 class: if viewport_mode() == HeaderViewportMode::Mobile { "showcase-layout-phone-wrap" } else { "w-full" },
367 div {
368 class: if viewport_mode() == HeaderViewportMode::Mobile {
369 "showcase-mobile-phone-frame"
370 } else {
371 "showcase-layout-window showcase-detail-window-scroll"
372 },
373 "data-showcase-preview": "header",
374
375 if viewport_mode() == HeaderViewportMode::Mobile {
376 div { class: "showcase-phone-notch",
377 span { "09:41" }
378 div { class: "showcase-phone-notch-pill" }
379 span { "5G 100%" }
380 }
381 }
382
383 Header {
384 nav_items: build_nav_items(sample_route(active())),
385 right_content: rsx! { {search_button} },
386 max_width: if viewport_mode() == HeaderViewportMode::Mobile { "w-full" } else { max_width_choice() },
387 menu_id,
388 page_shell: false,
389 on_demo_navigate: move |label| active.set(label),
390 }
391
392 div {
393 class: if viewport_mode() == HeaderViewportMode::Mobile {
394 "showcase-phone-scroll p-4 space-y-4"
395 } else {
396 "flex-1 p-6 md:p-8 space-y-6 max-w-4xl mx-auto w-full"
397 },
398 div { class: "rounded-2xl border border-[var(--sc-line)] bg-paper-entry/40 p-5",
399 div { class: "flex items-center justify-between",
400 span { class: "text-xs font-mono text-paper-secondary uppercase tracking-wider", "当前演示栏目" }
401 span { class: "text-xs px-2.5 py-0.5 rounded-full bg-paper-accent-soft text-paper-accent font-semibold", "{active()}" }
402 }
403 h3 { class: "text-lg font-semibold mt-2 text-paper-primary",
404 match active() {
405 "首页" => "首页 · 最新动态与精选文章",
406 "笔记" => "笔记 · 碎片记录与灵感闪光",
407 "归档" => "归档 · 按年份组织的历史文章",
408 "友链" => "友链 · 互联互通的网络邻居",
409 "关于" => "关于 · 站点介绍与设计故事",
410 "搜索" => "搜索 · 全局快速文本检索",
411 _ => "栏目演示",
412 }
413 }
414 p { class: "text-xs text-paper-secondary mt-1.5 leading-relaxed",
415 if viewport_mode() == HeaderViewportMode::Mobile {
416 "点击右上角汉堡按钮,可展开移动端全屏抽屉菜单;点击任意栏目自动收起并切换当前状态。"
417 } else {
418 "点击上方导航栏项目可切换激活项;向下滚动本区域可体验 Header 粘性吸顶 (sticky) 与磨砂毛玻璃 (backdrop-blur) 效果。"
419 }
420 }
421 }
422
423 div { class: "rounded-2xl border border-[var(--sc-line)]/60 bg-paper-entry/20 p-5 space-y-3",
424 p { class: "text-xs font-mono text-paper-secondary tracking-wider", "FEATURE HIGHLIGHTS / 特性说明" }
425 ul { class: "text-xs text-paper-secondary space-y-2 list-disc list-inside",
426 li { "自适应居中对齐:通过 max_width 参数确保顶部导航项左右边缘与下方文章正文精准对齐。" }
427 li { "指示器平滑过渡:激活项下方带独立的 nav-indicator 下划线,栏目切换时平滑滑动。" }
428 li { "移动端交互:在移动端或窄屏视口下自动转换为旋转图标汉堡按钮与平滑展开面板。" }
429 }
430 }
431
432 div { class: "py-12 text-center text-xs font-mono text-paper-tertiary",
433 "— 往下滚动查看 Header 磨砂吸顶效果 —"
434 }
435 }
436 }
437 }
438 }
439 }
440}
441
442#[derive(Clone, Copy, PartialEq, Eq)]
443enum SearchSceneMode {
444 HeaderNav,
445 SearchPalette,
446 DesignSpecs,
447}
448
449struct SampleSearchResult {
450 title: &'static str,
451 snippet: &'static str,
452 tag: &'static str,
453 date: &'static str,
454}
455
456const SAMPLE_SEARCH_ITEMS: &[SampleSearchResult] = &[
457 SampleSearchResult {
458 title: "让想法生根:Yggdrasil 设计体系与架构总览",
459 snippet: "在时间的缝隙里,种下一些文字。探讨 Rust、Dioxus 与响应式设计。",
460 tag: "设计",
461 date: "2026-09-23",
462 },
463 SampleSearchResult {
464 title: "响应式导航栏与自适应平滑指示器实践",
465 snippet: "如何通过 nav-indicator 独立下划线实现多栏目与搜索入口间的平滑视觉对齐。",
466 tag: "前端",
467 date: "2026-09-18",
468 },
469 SampleSearchResult {
470 title: "Dioxus 0.7 全栈组件化重构笔记",
471 snippet: "从桌面端全宽到移动端 380px 折叠抽屉,打造极致纯粹的 Web 交互体验。",
472 tag: "Rust",
473 date: "2026-09-10",
474 },
475 SampleSearchResult {
476 title: "Tailwind CSS v4 现代暗黑模式调色板指南",
477 snippet: "基于 currentColor 与 paper 主题语义变量的自适应图标与组件规范。",
478 tag: "样式",
479 date: "2026-08-28",
480 },
481];
482
483#[component]
484fn SearchIconLinkPreview(detail: bool) -> Element {
485 let mut scene_mode = use_signal(|| SearchSceneMode::HeaderNav);
486 let mut is_active_route = use_signal(|| false);
487 let mut query = use_signal(String::new);
488
489 if !detail {
491 return rsx! {
492 div {
493 class: "showcase-layout-window flex flex-col justify-between p-3 select-none h-full",
494 "data-showcase-preview": "search-icon-link",
495
496 div { class: "rounded-xl border border-[var(--sc-line)] bg-paper-entry/60 px-3 py-2 flex items-center justify-between shadow-xs",
498 div { class: "flex items-center gap-2",
499 span { class: "w-2 h-2 rounded-full bg-paper-accent animate-pulse" }
500 span { class: "text-xs font-serif font-bold text-paper-primary tracking-wide", "Yggdrasil" }
501 div { class: "hidden sm:flex items-center gap-1.5 ml-1 text-[10px] text-paper-secondary/80",
502 span { class: "px-1.5 py-0.5 rounded bg-paper-bg/60 border border-[var(--sc-line)]/50", "首页" }
503 span { class: "px-1.5 py-0.5 rounded bg-paper-bg/60 border border-[var(--sc-line)]/50", "笔记" }
504 }
505 }
506 div { class: "flex items-center gap-1",
507 SearchIconLink { active: Some(false) }
509 ThemeToggle {}
510 }
511 }
512
513 div { class: "grid grid-cols-2 gap-2 my-auto py-1",
515 div { class: "rounded-lg border border-[var(--sc-line)]/60 bg-paper-entry/30 p-2 flex items-center gap-2",
516 div { class: "shrink-0 scale-90 -m-1 pointer-events-none",
517 SearchIconLink { active: Some(false) }
518 }
519 div { class: "min-w-0 flex flex-col",
520 span { class: "text-[11px] font-mono text-paper-secondary font-medium", "默认状态" }
521 span { class: "text-[10px] text-paper-tertiary truncate", "次级色 · 悬停高亮" }
522 }
523 }
524 div { class: "rounded-lg border border-paper-accent/30 bg-paper-accent/5 p-2 flex items-center gap-2",
525 div { class: "shrink-0 scale-90 -m-1 pointer-events-none",
526 SearchIconLink { active: Some(true) }
527 }
528 div { class: "min-w-0 flex flex-col",
529 span { class: "text-[11px] font-mono text-paper-accent font-medium", "激活状态" }
530 span { class: "text-[10px] text-paper-accent/80 truncate", "下划线 · /search" }
531 }
532 }
533 }
534
535 div { class: "flex items-center justify-between text-[10px] text-paper-secondary/80 pt-1.5 border-t border-[var(--sc-line)]/40 font-mono",
537 span { "ARIA 语义友好" }
538 span { "快捷键 ⌘K" }
539 span { "40px 规范热区" }
540 }
541 }
542 };
543 }
544
545 let current_q = query().trim().to_lowercase();
547 let mock_url = if is_active_route() {
548 "https://yggdrasil.site/search"
549 } else {
550 "https://yggdrasil.site/about"
551 };
552 let filtered_items: Vec<_> = SAMPLE_SEARCH_ITEMS
553 .iter()
554 .filter(|item| {
555 if current_q.is_empty() {
556 true
557 } else {
558 item.title.to_lowercase().contains(¤t_q)
559 || item.snippet.to_lowercase().contains(¤t_q)
560 || item.tag.to_lowercase().contains(¤t_q)
561 }
562 })
563 .collect();
564
565 rsx! {
566 div { class: "w-full flex flex-col gap-4",
567 div { class: "flex flex-wrap items-center justify-between gap-3 p-3 rounded-xl bg-paper-entry/50 border border-[var(--sc-line)]",
569 div { class: "flex items-center gap-2",
570 span { class: "text-xs font-mono text-paper-secondary tracking-wider", "SCENE / 演示场景" }
571 div { class: "showcase-picker-mode-switch",
572 button {
573 r#type: "button",
574 aria_pressed: "{scene_mode() == SearchSceneMode::HeaderNav}",
575 onclick: move |_| scene_mode.set(SearchSceneMode::HeaderNav),
576 "🖥️ 导航栏场景"
577 }
578 button {
579 r#type: "button",
580 aria_pressed: "{scene_mode() == SearchSceneMode::SearchPalette}",
581 onclick: move |_| scene_mode.set(SearchSceneMode::SearchPalette),
582 "🔍 检索面板联动"
583 }
584 button {
585 r#type: "button",
586 aria_pressed: "{scene_mode() == SearchSceneMode::DesignSpecs}",
587 onclick: move |_| scene_mode.set(SearchSceneMode::DesignSpecs),
588 "📐 规范与无障碍"
589 }
590 }
591 }
592
593 div { class: "flex items-center gap-2",
594 span { class: "text-xs font-mono text-paper-secondary tracking-wider", "ROUTE / 路由模拟" }
595 div { class: "showcase-picker-mode-switch",
596 button {
597 r#type: "button",
598 aria_pressed: "{!is_active_route()}",
599 onclick: move |_| is_active_route.set(false),
600 "常规页面 (/about)"
601 }
602 button {
603 r#type: "button",
604 aria_pressed: "{is_active_route()}",
605 onclick: move |_| is_active_route.set(true),
606 "搜索页面 (/search)"
607 }
608 }
609 }
610 }
611
612 div { class: "rounded-2xl border border-[var(--sc-line)] bg-paper-theme overflow-hidden shadow-xs",
614 match scene_mode() {
615 SearchSceneMode::HeaderNav => rsx! {
617 div { class: "p-6 flex flex-col gap-6",
618 div { class: "rounded-xl border border-[var(--sc-line)] bg-paper-entry/30 overflow-hidden shadow-xs",
619 div { class: "flex items-center justify-between px-3.5 py-2 border-b border-[var(--sc-line)] bg-paper-entry/50 text-[11px] font-mono text-paper-secondary",
621 div { class: "flex items-center gap-1.5",
622 span { class: "w-2.5 h-2.5 rounded-full bg-rose-500/70" }
623 span { class: "w-2.5 h-2.5 rounded-full bg-amber-500/70" }
624 span { class: "w-2.5 h-2.5 rounded-full bg-emerald-500/70" }
625 }
626 span { "{mock_url}" }
627 span { class: "text-[10px] text-paper-tertiary", "Header 预览" }
628 }
629
630 div { class: "px-4 md:px-6 py-2.5 flex items-center justify-between bg-paper-theme/80 backdrop-blur-sm",
632 div { class: "flex items-center gap-2",
634 span { class: "w-2 h-2 rounded-full bg-paper-accent" }
635 span { class: "font-serif font-bold text-sm tracking-wide text-paper-primary", "Yggdrasil" }
636 }
637
638 div { class: "hidden md:flex items-center gap-4 text-xs font-medium text-paper-secondary",
640 span { class: if !is_active_route() { "text-paper-accent relative pb-1 border-b-2 border-paper-accent" } else { "hover:text-paper-primary transition-colors cursor-pointer" }, "关于" }
641 span { class: "hover:text-paper-primary transition-colors cursor-pointer", "首页" }
642 span { class: "hover:text-paper-primary transition-colors cursor-pointer", "笔记" }
643 span { class: "hover:text-paper-primary transition-colors cursor-pointer", "归档" }
644 }
645
646 div { class: "flex items-center gap-2",
648 div {
649 class: "relative cursor-pointer group flex items-center justify-center",
650 title: "点击可切换激活状态",
651 SearchIconLink {
653 active: Some(is_active_route()),
654 on_click: move |_| is_active_route.toggle(),
655 }
656 span { class: "absolute -top-1 -right-1 flex h-2 w-2",
658 span { class: "animate-ping absolute inline-flex h-full w-full rounded-full bg-paper-accent opacity-60" }
659 span { class: "relative inline-flex rounded-full h-2 w-2 bg-paper-accent" }
660 }
661 }
662 div { class: "text-paper-secondary",
663 ThemeToggle {}
664 }
665 }
666 }
667 }
668
669 div { class: "grid grid-cols-1 md:grid-cols-3 gap-3 text-xs",
671 div { class: "p-3.5 rounded-xl border border-[var(--sc-line)] bg-paper-entry/40",
672 p { class: "font-mono text-[10px] text-paper-secondary tracking-wider", "CURRENT STATUS / 当前状态" }
673 p { class: "font-semibold text-paper-primary mt-1 flex items-center gap-1.5",
674 if is_active_route() {
675 span { class: "w-2 h-2 rounded-full bg-paper-accent" }
676 "已激活 · 位于搜索页 (/search)"
677 } else {
678 span { class: "w-2 h-2 rounded-full bg-paper-secondary" }
679 "默认待命 · 常规页面 (/about)"
680 }
681 }
682 p { class: "text-[11px] text-paper-secondary mt-1 leading-relaxed",
683 if is_active_route() {
684 "当前图标呈现 text-paper-accent,底部挂载独立的 nav-indicator 绿色下划线指示条。"
685 } else {
686 "当前图标呈现 text-paper-secondary,鼠标悬浮时平滑过渡至强调色。"
687 }
688 }
689 }
690
691 div { class: "p-3.5 rounded-xl border border-[var(--sc-line)] bg-paper-entry/40",
692 p { class: "font-mono text-[10px] text-paper-secondary tracking-wider", "A11Y SEMANTICS / 无障碍语义" }
693 div { class: "mt-1 space-y-1 font-mono text-[11px] text-paper-primary",
694 p { "aria-label = \"搜索\"" }
695 p { "title = \"搜索\"" }
696 p { if is_active_route() { "aria-current = \"page\"" } else { "aria-current = (none)" } }
697 }
698 p { class: "text-[11px] text-paper-secondary mt-1", "屏幕阅读器与辅助工具可精准感知当前定位。" }
699 }
700
701 div { class: "p-3.5 rounded-xl border border-[var(--sc-line)] bg-paper-entry/40 flex flex-col justify-between",
702 div {
703 p { class: "font-mono text-[10px] text-paper-secondary tracking-wider", "INTERACTION / 交互提示" }
704 p { class: "text-xs text-paper-primary font-medium mt-1", "点击上方搜索图标直接联动" }
705 p { class: "text-[11px] text-paper-secondary mt-0.5", "通过 on_click 事件拦截即可就地演示激活与指示条过渡效果。" }
706 }
707 button {
708 r#type: "button",
709 class: "mt-2 self-start px-2.5 py-1 text-[11px] rounded-lg bg-paper-entry border border-[var(--sc-line)] text-paper-primary hover:border-paper-accent transition-colors",
710 onclick: move |_| is_active_route.toggle(),
711 "↻ 切换激活状态"
712 }
713 }
714 }
715 }
716 },
717
718 SearchSceneMode::SearchPalette => rsx! {
720 div { class: "p-6 flex flex-col gap-4",
721 div { class: "flex items-center justify-between text-xs text-paper-secondary font-mono",
722 span { "SEARCH OVERLAY / 模拟点击搜索入口后唤起的快捷检索面板" }
723 span { "共收录 {SAMPLE_SEARCH_ITEMS.len()} 篇样例索引" }
724 }
725
726 div { class: "rounded-2xl border border-[var(--sc-line)] bg-paper-entry/40 backdrop-blur-md p-4 sm:p-5 flex flex-col gap-3 shadow-md",
728 div { class: "relative flex items-center",
730 span { class: "absolute left-3.5 text-paper-secondary",
731 svg {
732 xmlns: "http://www.w3.org/2000/svg",
733 class: "w-4 h-4",
734 view_box: "0 0 24 24",
735 fill: "none",
736 stroke: "currentColor",
737 stroke_width: "1.6",
738 stroke_linecap: "round",
739 stroke_linejoin: "round",
740 "aria-hidden": "true",
741 circle { cx: "10.75", cy: "10.75", r: "6.75" }
742 path { d: "m16 16 4.5 4.5" }
743 }
744 }
745 input {
746 class: "w-full pl-10 pr-20 py-2.5 text-sm rounded-xl border border-[var(--sc-line)] bg-paper-theme text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30 transition-all",
747 r#type: "text",
748 placeholder: "输入关键词快速检索,如 Rust、设计、Dioxus…",
749 value: "{query()}",
750 oninput: move |e| query.set(e.value()),
751 }
752 div { class: "absolute right-3 flex items-center gap-1.5",
753 if !query().is_empty() {
754 button {
755 r#type: "button",
756 class: "text-xs text-paper-secondary hover:text-paper-primary p-1 rounded-md",
757 onclick: move |_| query.set(String::new()),
758 "✕"
759 }
760 }
761 span { class: "text-[10px] font-mono px-1.5 py-0.5 rounded border border-[var(--sc-line)] bg-paper-entry text-paper-tertiary", "ESC" }
762 }
763 }
764
765 div { class: "flex flex-wrap items-center gap-1.5 text-xs",
767 span { class: "text-[11px] text-paper-secondary mr-1", "快速填入:" }
768 for tag in &["Rust", "设计", "Dioxus", "Tailwind", "前端"] {
769 button {
770 r#type: "button",
771 class: "px-2 py-0.5 text-[11px] rounded-full border border-[var(--sc-line)] bg-paper-theme/60 text-paper-secondary hover:text-paper-accent hover:border-paper-accent transition-colors",
772 onclick: {
773 let t = tag.to_string();
774 move |_| query.set(t.clone())
775 },
776 "{tag}"
777 }
778 }
779 }
780
781 div { class: "mt-1 flex flex-col gap-2 max-h-60 overflow-y-auto pr-1",
783 if filtered_items.is_empty() {
784 div { class: "py-8 text-center text-xs text-paper-secondary",
785 "没有匹配到相关结果,换个关键词试试。"
786 }
787 } else {
788 for (idx, item) in filtered_items.iter().enumerate() {
789 div {
790 key: "{idx}",
791 class: "p-3 rounded-xl border border-[var(--sc-line)]/60 bg-paper-theme/40 hover:bg-paper-theme/80 hover:border-paper-accent/50 transition-all flex flex-col gap-1 cursor-pointer",
792 div { class: "flex items-center justify-between gap-2",
793 h4 { class: "text-xs font-semibold text-paper-primary truncate", "{item.title}" }
794 div { class: "flex items-center gap-1.5 shrink-0",
795 span { class: "text-[10px] font-mono text-paper-tertiary", "{item.date}" }
796 span { class: "text-[10px] font-mono px-1.5 py-0.5 rounded bg-paper-entry text-paper-secondary", "{item.tag}" }
797 }
798 }
799 p { class: "text-[11px] text-paper-secondary line-clamp-1", "{item.snippet}" }
800 }
801 }
802 }
803 }
804 }
805 }
806 },
807
808 SearchSceneMode::DesignSpecs => rsx! {
810 div { class: "p-6 flex flex-col gap-6",
811 div { class: "grid grid-cols-1 md:grid-cols-2 gap-4",
813 div { class: "p-6 rounded-2xl border border-[var(--sc-line)] bg-paper-entry/30 flex flex-col items-center justify-center gap-3 text-center",
815 div { class: "relative p-4 rounded-full border-2 border-dashed border-paper-border/60 bg-paper-theme shadow-xs",
816 SearchIconLink { active: Some(false) }
817 span { class: "absolute -bottom-2 px-2 py-0.5 text-[9px] font-mono rounded bg-paper-entry text-paper-secondary border border-[var(--sc-line)]", "40×40px" }
818 }
819 div {
820 h4 { class: "text-sm font-semibold text-paper-primary", "默认待命态 (Default State)" }
821 p { class: "text-xs text-paper-secondary mt-1", "柔和次级色调 text-paper-secondary,提供平滑悬停过渡色" }
822 }
823 }
824
825 div { class: "p-6 rounded-2xl border border-paper-accent/30 bg-paper-accent/5 flex flex-col items-center justify-center gap-3 text-center",
827 div { class: "relative p-4 rounded-full border-2 border-dashed border-paper-accent/40 bg-paper-theme shadow-xs",
828 SearchIconLink { active: Some(true) }
829 span { class: "absolute -bottom-2 px-2 py-0.5 text-[9px] font-mono rounded bg-paper-accent text-white", "Active" }
830 }
831 div {
832 h4 { class: "text-sm font-semibold text-paper-accent", "路由激活态 (Active State)" }
833 p { class: "text-xs text-paper-secondary mt-1", "强调色 text-paper-accent,底部显示独立的 nav-indicator 指示条" }
834 }
835 }
836 }
837
838 div { class: "rounded-xl border border-[var(--sc-line)] overflow-hidden text-xs",
840 div { class: "grid grid-cols-3 bg-paper-entry/60 px-4 py-2 font-mono text-[11px] text-paper-secondary border-b border-[var(--sc-line)]",
841 span { "参数维度" }
842 span { "实现规范" }
843 span { "设计目的" }
844 }
845 div { class: "divide-y divide-[var(--sc-line)] bg-paper-theme/50",
846 div { class: "grid grid-cols-3 px-4 py-2.5 items-center",
847 span { class: "font-mono font-medium text-paper-primary", "Touch Target" }
848 span { class: "text-paper-secondary font-mono", "40px × 40px (p-2 rounded-full)" }
849 span { class: "text-paper-secondary", "符合 WCAG 移动触控最小热区标准" }
850 }
851 div { class: "grid grid-cols-3 px-4 py-2.5 items-center",
852 span { class: "font-mono font-medium text-paper-primary", "Vector Icon" }
853 span { class: "text-paper-secondary font-mono", "24px × 24px (统一线性放大镜)" }
854 span { class: "text-paper-secondary", "与 /search 搜索页面图标风格保持 100% 呼应" }
855 }
856 div { class: "grid grid-cols-3 px-4 py-2.5 items-center",
857 span { class: "font-mono font-medium text-paper-primary", "Color & Stroke" }
858 span { class: "text-paper-secondary font-mono", "stroke=\"currentColor\"" }
859 span { class: "text-paper-secondary", "1.6px 极简线条,跟随纸质主题自适应着色" }
860 }
861 div { class: "grid grid-cols-3 px-4 py-2.5 items-center",
862 span { class: "font-mono font-medium text-paper-primary", "Indicator" }
863 span { class: "text-paper-secondary font-mono", "nav-indicator (hidden md:block)" }
864 span { class: "text-paper-secondary", "与文字导航保持一致的激活视觉语法" }
865 }
866 }
867 }
868 }
869 },
870 }
871 }
872
873 div { class: "grid grid-cols-1 md:grid-cols-3 gap-3.5",
875 div { class: "p-4 rounded-xl border border-[var(--sc-line)]/70 bg-paper-entry/20 flex flex-col gap-1.5",
876 div { class: "flex items-center gap-1.5 text-paper-accent font-medium text-xs",
877 span { "🌲" }
878 span { "路由自动感知" }
879 }
880 p { class: "text-xs text-paper-secondary leading-relaxed",
881 "组件内部通过 use_route::<Route>() 自动感知是否匹配 Route::Search,零额外代码即可同步高亮状态。"
882 }
883 }
884
885 div { class: "p-4 rounded-xl border border-[var(--sc-line)]/70 bg-paper-entry/20 flex flex-col gap-1.5",
886 div { class: "flex items-center gap-1.5 text-paper-accent font-medium text-xs",
887 span { "🎨" }
888 span { "图标组视觉统一" }
889 }
890 p { class: "text-xs text-paper-secondary leading-relaxed",
891 "与 ThemeToggle 主题切换按钮共享一致的内边距、圆角与过渡时长,维持导航栏右侧图标组严格对齐。"
892 }
893 }
894
895 div { class: "p-4 rounded-xl border border-[var(--sc-line)]/70 bg-paper-entry/20 flex flex-col gap-1.5",
896 div { class: "flex items-center gap-1.5 text-paper-accent font-medium text-xs",
897 span { "♿" }
898 span { "无障碍可达性" }
899 }
900 p { class: "text-xs text-paper-secondary leading-relaxed",
901 "内置 aria-label=\"搜索\" 与 aria-current 状态宣告,全键盘操作友好,支持右键新标签页打开。"
902 }
903 }
904 }
905 }
906 }
907}
908
909#[component]
910fn FooterPreview(detail: bool) -> Element {
911 let scroll_id = if detail {
912 "showcase-footer-scroll-detail"
913 } else {
914 "showcase-footer-scroll-card"
915 };
916 rsx! {
917 div { class: "showcase-layout-window", "data-showcase-preview": "footer",
918 div { id: scroll_id, class: "showcase-footer-scroll",
919 p { "页脚会陪你到页面尽头。" }
920 if detail {
921 p { "向下滚动这一小块内容,再点击页脚右侧按钮,就会回到演示顶部。" }
922 div { style: "height: 180px" }
923 p { "这里是演示内容的末尾。" }
924 }
925 }
926 FooterView {
927 github_url: Some("https://github.com".to_string()),
928 top_visible: true,
929 page_shell: false,
930 inline_top: true,
931 on_top: move |_| scroll_demo_to_top(scroll_id),
932 }
933 }
934 }
935}
936
937#[derive(Clone, Copy, PartialEq, Eq)]
938enum FrontendLayoutMode {
939 Desktop,
940 Mobile,
941 Skeleton,
942}
943
944#[component]
945fn FrontendLayoutPreview(detail: bool) -> Element {
946 let mut active = use_signal(|| "首页");
947 let mut layout_mode = use_signal(|| FrontendLayoutMode::Desktop);
948 let mut max_width_choice = use_signal(|| "max-w-4xl");
949 let menu_id = if detail {
950 "mobile-nav-menu-showcase-layout-detail"
951 } else {
952 "mobile-nav-menu-showcase-layout-card"
953 };
954 let scroll_id = if detail {
955 "showcase-frontend-scroll-detail"
956 } else {
957 "showcase-frontend-scroll-card"
958 };
959
960 let search_button = rsx! {
961 button {
962 class: if active() == "搜索" {
963 "p-2 rounded-full text-paper-accent transition-colors duration-200"
964 } else {
965 "p-2 rounded-full text-paper-secondary hover:text-paper-accent transition-colors duration-200"
966 },
967 r#type: "button",
968 aria_label: "演示搜索操作",
969 onclick: move |_| active.set("搜索"),
970 svg {
971 xmlns: "http://www.w3.org/2000/svg",
972 class: "w-4 h-4 md:w-5 md:h-5",
973 view_box: "0 -960 960 960",
974 fill: "currentColor",
975 path { d: "M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z" }
976 }
977 }
978 };
979
980 if !detail {
981 return rsx! {
982 div { id: scroll_id, class: "showcase-layout-window", "data-showcase-preview": "frontend-layout",
983 FrontendShell {
984 min_height: "h-full min-h-0",
985 max_width: "max-w-4xl",
986 header: rsx! {
987 Header {
988 nav_items: build_nav_items(sample_route(active())),
989 right_content: rsx! { {search_button} },
990 max_width: "max-w-4xl",
991 menu_id,
992 page_shell: false,
993 on_demo_navigate: move |label| active.set(label),
994 }
995 },
996 main_content: rsx! {
997 article { class: "flex flex-col justify-center h-full",
998 h2 { class: "font-semibold text-paper-primary", "一页内容,安心阅读" }
999 p { class: "text-paper-secondary mt-1", "前台布局将导航、正文和页脚排成清晰的阅读路线。" }
1000 }
1001 },
1002 footer: rsx! {
1003 FooterView {
1004 github_url: Some("https://github.com".to_string()),
1005 top_visible: false,
1006 page_shell: false,
1007 inline_top: true,
1008 on_top: move |_| scroll_demo_to_top(scroll_id),
1009 }
1010 },
1011 }
1012 }
1013 };
1014 }
1015
1016 rsx! {
1017 div { class: "w-full flex flex-col gap-3",
1018 div { class: "flex flex-wrap items-center justify-between gap-3 p-3 rounded-xl bg-paper-entry/50 border border-[var(--sc-line)]",
1019 div { class: "flex items-center gap-2",
1020 span { class: "text-xs font-mono text-paper-secondary tracking-wider", "LAYOUT" }
1021 div { class: "showcase-picker-mode-switch",
1022 button {
1023 r#type: "button",
1024 aria_pressed: "{layout_mode() == FrontendLayoutMode::Desktop}",
1025 onclick: move |_| layout_mode.set(FrontendLayoutMode::Desktop),
1026 "🖥️ 完整页面"
1027 }
1028 button {
1029 r#type: "button",
1030 aria_pressed: "{layout_mode() == FrontendLayoutMode::Mobile}",
1031 onclick: move |_| layout_mode.set(FrontendLayoutMode::Mobile),
1032 "📱 移动端布局"
1033 }
1034 button {
1035 r#type: "button",
1036 aria_pressed: "{layout_mode() == FrontendLayoutMode::Skeleton}",
1037 onclick: move |_| layout_mode.set(FrontendLayoutMode::Skeleton),
1038 "🦴 骨架屏加载"
1039 }
1040 }
1041 }
1042 if layout_mode() == FrontendLayoutMode::Desktop {
1043 div { class: "flex items-center gap-2",
1044 span { class: "text-xs font-mono text-paper-secondary tracking-wider", "WIDTH" }
1045 div { class: "showcase-picker-mode-switch",
1046 button {
1047 r#type: "button",
1048 aria_pressed: "{max_width_choice() == \"max-w-4xl\"}",
1049 onclick: move |_| max_width_choice.set("max-w-4xl"),
1050 "4xl (896px 常规)"
1051 }
1052 button {
1053 r#type: "button",
1054 aria_pressed: "{max_width_choice() == \"max-w-6xl\"}",
1055 onclick: move |_| max_width_choice.set("max-w-6xl"),
1056 "6xl (1152px 宽屏)"
1057 }
1058 }
1059 }
1060 }
1061 }
1062
1063 div {
1064 class: if layout_mode() == FrontendLayoutMode::Mobile { "showcase-layout-phone-wrap" } else { "w-full" },
1065 div {
1066 id: scroll_id,
1067 class: if layout_mode() == FrontendLayoutMode::Mobile {
1068 "showcase-mobile-phone-frame"
1069 } else {
1070 "showcase-layout-window showcase-detail-window-scroll"
1071 },
1072 "data-showcase-preview": "frontend-layout",
1073
1074 if layout_mode() == FrontendLayoutMode::Mobile {
1075 div { class: "showcase-phone-notch",
1076 span { "09:41" }
1077 div { class: "showcase-phone-notch-pill" }
1078 span { "5G 100%" }
1079 }
1080 }
1081
1082 FrontendShell {
1083 min_height: "min-h-[480px]",
1084 max_width: if layout_mode() == FrontendLayoutMode::Mobile { "w-full" } else { max_width_choice() },
1085 header: rsx! {
1086 Header {
1087 nav_items: build_nav_items(sample_route(active())),
1088 right_content: rsx! { {search_button} },
1089 max_width: if layout_mode() == FrontendLayoutMode::Mobile { "w-full" } else { max_width_choice() },
1090 menu_id,
1091 page_shell: false,
1092 on_demo_navigate: move |label| active.set(label),
1093 }
1094 },
1095 main_content: rsx! {
1096 if layout_mode() == FrontendLayoutMode::Skeleton {
1097 div { class: "space-y-4 py-4",
1098 div { class: "p-3 rounded-xl bg-paper-accent-soft text-paper-accent text-xs font-mono flex items-center justify-between",
1099 span { "SUSPENSE BOUNDARY SKELETON PLACEHOLDER" }
1100 span { "路由加载中" }
1101 }
1102 HomeSkeleton { current_page: 1 }
1103 }
1104 } else {
1105 article { class: "space-y-6",
1106 div { class: "border-b border-[var(--sc-line)] pb-5",
1107 span { class: "text-xs font-mono text-paper-secondary", "FRONTEND LAYOUT SPECIMEN" }
1108 h2 { class: "text-2xl font-bold tracking-tight text-paper-primary mt-1",
1109 match active() {
1110 "首页" => "一页内容,安心阅读",
1111 "笔记" => "公开笔记 · 随笔与灵感",
1112 "归档" => "岁月归档 · 时间线检索",
1113 "友链" => "友情链接 · 志趣相投的伙伴",
1114 "关于" => "关于本站 · 架构设计与理念",
1115 "搜索" => "站内检索 · 快速定位内容",
1116 _ => "前台内容展示",
1117 }
1118 }
1119 p { class: "mt-2 text-sm text-paper-secondary leading-relaxed",
1120 "前台布局通过统一的 FrontendShell 组合顶部 Header、弹性内容主区 main 与底部 Footer。当前选中栏目:{active()}。"
1121 }
1122 }
1123
1124 div { class: "space-y-4",
1125 div { class: "p-5 rounded-2xl border border-[var(--sc-line)] bg-paper-entry/40 hover:bg-paper-entry/70 transition-colors",
1126 div { class: "flex items-center gap-2 text-xs text-paper-secondary font-mono",
1127 span { class: "px-2 py-0.5 rounded-full bg-paper-accent-soft text-paper-accent font-medium", "精选文章" }
1128 span { "•" }
1129 span { "2026-09-28" }
1130 span { "•" }
1131 span { "5 分钟阅读" }
1132 }
1133 h3 { class: "text-lg font-semibold text-paper-primary mt-2", "Dioxus 0.7 与全栈 Rust 博客布局演进" }
1134 p { class: "text-xs text-paper-secondary mt-1.5 leading-relaxed",
1135 "探讨全栈 Rust 前端在 SSR、水合、骨架屏与页面过渡动画上的工程实践,兼顾极致的加载性能与阅读体验。"
1136 }
1137 }
1138
1139 div { class: "p-5 rounded-2xl border border-[var(--sc-line)] bg-paper-entry/40 hover:bg-paper-entry/70 transition-colors",
1140 div { class: "flex items-center gap-2 text-xs text-paper-secondary font-mono",
1141 span { class: "px-2 py-0.5 rounded-full bg-paper-entry text-paper-secondary border border-[var(--sc-line)]", "设计系统" }
1142 span { "•" }
1143 span { "2026-09-15" }
1144 }
1145 h3 { class: "text-lg font-semibold text-paper-primary mt-2", "统一宽度与响应式对齐的排版艺术" }
1146 p { class: "text-xs text-paper-secondary mt-1.5 leading-relaxed",
1147 "导航栏 max-w 与正文容器严格对齐,保证视口放大缩小时视觉焦点稳定不飘移。"
1148 }
1149 }
1150 }
1151
1152 div { class: "pt-8 pb-4 text-center text-xs font-mono text-paper-tertiary",
1153 "— 滚动至底部可点击页脚回到顶部 —"
1154 }
1155 }
1156 }
1157 },
1158 footer: rsx! {
1159 FooterView {
1160 github_url: Some("https://github.com".to_string()),
1161 top_visible: true,
1162 page_shell: false,
1163 inline_top: true,
1164 on_top: move |_| scroll_demo_to_top(scroll_id),
1165 }
1166 },
1167 }
1168 }
1169 }
1170 }
1171 }
1172}
1173
1174fn scroll_demo_to_top(id: &str) {
1175 #[cfg(target_arch = "wasm32")]
1176 if let Some(element) = web_sys::window()
1177 .and_then(|window| window.document())
1178 .and_then(|document| document.get_element_by_id(id))
1179 {
1180 element.set_scroll_top(0);
1181 }
1182 #[cfg(not(target_arch = "wasm32"))]
1183 let _ = id;
1184}
1185
1186fn article_html(prefix: &str, alternate: bool) -> String {
1187 let opening = if alternate {
1188 "雨停之后,树叶留下细小的光。"
1189 } else {
1190 "一片叶子,也记得来时的风。"
1191 };
1192 format!(
1193 r#"<h2 id="{prefix}-opening">从一片叶子开始</h2>
1194<p>{opening} 这段固定文章展示正文排版和图片,不连接任何服务。</p>
1195<h3 id="{prefix}-details">沿途所见</h3>
1196<ul><li>清晨的光</li><li>缓慢生长的枝条</li></ul>
1197<blockquote><p>让想法,像树一样生长。</p></blockquote>
1198<pre><code class="language-rust">let leaf = "Yggdrasil";
1199println!("{{leaf}}");</code></pre>
1200<h2 id="{prefix}-image">留下一张照片</h2>
1201<p><img src="/images/empty-state/dog-camera.webp" alt="相机里的两只小狗" loading="lazy"></p>"#
1202 )
1203}
1204
1205fn toc_html(prefix: &str) -> String {
1206 format!(
1207 r##"<ul><li><a href="#{prefix}-opening">从一片叶子开始</a><ul><li><a href="#{prefix}-details">沿途所见</a></li></ul></li><li><a href="#{prefix}-image">留下一张照片</a></li></ul>"##
1208 )
1209}
1210
1211#[component]
1212fn PostContentPreview(detail: bool) -> Element {
1213 let mut alternate = use_signal(|| false);
1214 let (scope_id, heading_prefix) = if detail {
1215 (
1216 "showcase-post-content-detail",
1217 "showcase-post-content-detail",
1218 )
1219 } else {
1220 ("showcase-post-content-card", "showcase-post-content-card")
1221 };
1222 rsx! {
1223 div { class: "showcase-article-window", "data-showcase-preview": "post-content",
1224 if detail {
1225 button {
1226 class: "mb-4 text-sm text-paper-accent",
1227 r#type: "button",
1228 onclick: move |_| alternate.toggle(),
1229 "切换文章样例"
1230 }
1231 }
1232 for variant in std::iter::once(alternate()) {
1233 PostContent {
1234 key: "sample-{variant}",
1235 content_html: article_html(heading_prefix, variant),
1236 scope_id: Some(scope_id),
1237 }
1238 }
1239 }
1240 }
1241}
1242
1243#[component]
1244fn PostFooterPreview(detail: bool) -> Element {
1245 let mut post = super::sample_post();
1246 post.tags = vec!["设计".to_string(), "阅读".to_string(), "Rust".to_string()];
1247 post.prev_post = Some(PostNav {
1248 title: "沿着风的方向".to_string(),
1249 slug: "sample-previous".to_string(),
1250 });
1251 post.next_post = Some(PostNav {
1252 title: "下一圈年轮".to_string(),
1253 slug: "sample-next".to_string(),
1254 });
1255 rsx! {
1256 div { class: "showcase-article-window", "data-showcase-preview": "post-footer",
1257 PostFooter { post, sample: true }
1258 if detail {
1259 p { class: "showcase-post-footer-note", "样例标签和相邻文章不跳转。" }
1260 }
1261 }
1262 }
1263}
1264
1265#[component]
1266fn PostNavLinksPreview(detail: bool) -> Element {
1267 let prev = PostNav {
1268 title: "一片叶子,落在代码之间".to_string(),
1269 slug: "sample-previous".to_string(),
1270 };
1271 let next = PostNav {
1272 title: "下一圈年轮:让想法慢慢长成一座数字花园".to_string(),
1273 slug: "sample-next".to_string(),
1274 };
1275
1276 rsx! {
1277 div { class: "showcase-post-nav-demo", "data-showcase-preview": "post-nav-links",
1278 section { class: "showcase-post-nav-sheet", "data-showcase-sample": "both",
1279 h3 { class: "showcase-post-nav-heading", "继续阅读" span { "沿着文字,走向下一页" } }
1280 PostNavLinks { prev: Some(prev.clone()), next: Some(next.clone()), sample: true }
1281 }
1282 if detail {
1283 div { class: "showcase-post-nav-examples",
1284 section { class: "showcase-post-nav-example", "data-showcase-sample": "next-only",
1285 h3 { "第一篇文章" span { "只有下一篇" } }
1286 PostNavLinks { prev: None, next: Some(next), sample: true }
1287 }
1288 section { class: "showcase-post-nav-example", "data-showcase-sample": "prev-only",
1289 h3 { "最后一篇文章" span { "只有上一篇" } }
1290 PostNavLinks { prev: Some(prev), next: None, sample: true }
1291 }
1292 section { class: "showcase-post-nav-empty", "data-showcase-sample": "empty",
1293 PostNavLinks { prev: None, next: None, sample: true }
1294 p { "没有相邻文章时,不展示导航入口。" }
1295 }
1296 }
1297 p { class: "showcase-post-nav-note", "试试悬停或用 Tab 聚焦卡片。样例链接停留在本页。" }
1298 }
1299 }
1300 }
1301}
1302
1303#[component]
1304fn PostTocPreview(detail: bool) -> Element {
1305 let (scroll_id, nav_id, content_id) = if detail {
1306 (
1307 "showcase-post-toc-scroll-detail",
1308 "showcase-post-toc-nav-detail",
1309 "showcase-post-toc-content-detail",
1310 )
1311 } else {
1312 (
1313 "showcase-post-toc-scroll-card",
1314 "showcase-post-toc-nav-card",
1315 "showcase-post-toc-content-card",
1316 )
1317 };
1318 rsx! {
1319 div {
1320 id: scroll_id,
1321 class: "showcase-toc-window showcase-article-window",
1322 "data-showcase-preview": "post-toc",
1323 "data-local-anchor-scroll": "true",
1324 PostToc {
1325 toc_html: toc_html(content_id),
1326 title: "文章目录",
1327 nav_id: Some(nav_id),
1328 content_id: Some(content_id),
1329 scroll_id: Some(scroll_id),
1330 }
1331 PostContent { content_html: article_html(content_id, false), scope_id: Some(content_id) }
1332 }
1333 }
1334}
1335
1336#[cfg(test)]
1337mod tests {
1338 use super::*;
1339
1340 #[test]
1341 fn article_and_toc_share_scoped_headings() {
1342 let html = article_html("sample", false);
1343 let toc = toc_html("sample");
1344 for id in ["sample-opening", "sample-details", "sample-image"] {
1345 assert!(html.contains(&format!("id=\"{id}\"")));
1346 assert!(toc.contains(&format!("href=\"#{id}\"")));
1347 }
1348 assert!(!html.contains("data-runnable"));
1349 }
1350
1351 #[test]
1352 fn alternate_article_changes_real_content() {
1353 assert_ne!(article_html("sample", false), article_html("sample", true));
1354 }
1355}